본문 바로가기
🍎 iOS/DevNote

[SwiftUI] CustomTabView 구현

by @Eddy 2024. 3. 4.
728x90

구현하게 된 이유

진행 중인 프로젝트 디자인은 기본 TabView와 유사했음에도, 기본 TabView로는 구현할 수 없었습니다.
'SwiftUI에서 TabView item들의 속성은 조정할 수 있게 해주지' 라는 생각이 많이 들었던.. 동시에 앞으로의 프로젝트가 험난하겠구나(?)가 느껴진 순간이었습니다.
 

기능 및 디자인

1. 기능: item 클릭 시, 추가 동작 제어
2. 디자인: 기본 CustomView와 유사. (이미지 사이즈만 조정)

기본 TabView와 이렇게 비슷한데..!

 
 

Model 코드

Enum으로 다음과 같은 모델을 만들었고, 필요한 Property를 추가했습니다.

//
//  CustomTabView.swift
//
//  Created by Eddy on 2/19/24.
//

import SwiftUI


public enum TabItem: CaseIterable {
    case left // tabItem 갯수만큼 case 추가
    case center
    case right

    var image: Image { // item 이미지
        switch self {
        case .left: .icons(.ic_home_outlined)
        case .center: .init(systemName: "plus.app.fill")
        case .right: .icons(.ic_my_outlined)
        }
    }
    
    var text: String { // item 텍스트
        switch self {
        case .left: "홈"
        case .center: ""
        case .right: "마이페이지"
        }
    }
    
    var selectedColor: Color { // item 선택시 foregroundStyle
        return .colors(.gray800)
    }
    
    var unsSelectedColor: Color { // item 미선택시 foregroundStyle
        return .colors(.gray300)
    }
    
    var imageSize: CGFloat { // 텍스트 유무에 따른 이미지 사이즈
        if text.isEmpty {
            return 42
        } else {
            return 24
        }
    }
}

 
 

CustomTabView 코드

//
//  CustomTabView.swift
//
//  Created by Eddy on 2/19/24.
//

import SwiftUI

public struct CustomTabView<Content: View>: View {
    
    // MARK: - Properties
    @Binding private var isSelectedItem: TabItem
    private let content: (TabItem) -> Content
    private let onTappedItem: ((TabItem) -> Void)?
    
    // MARK: - Init
    public init(isSelectedItem: Binding<TabItem>,
                @ViewBuilder content: @escaping (TabItem) -> Content,
                onTappedItem: ((TabItem) -> Void)? = nil) {
        self._isSelectedItem = isSelectedItem
        self.content = content
        self.onTappedItem = onTappedItem
    }
    
    // MARK: - Views
    public var body: some View {
        ZStack {
            content(isSelectedItem)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            
            VStack(spacing: 0) {
                Divider()
                    .frame(maxHeight: .infinity, alignment: .bottom)
                
                HStack {
                    buttons()
                }
                .frame(maxWidth: .infinity)
                .ignoresSafeArea(edges: .horizontal)
                .background(.white)
            }
        }
    }
}

fileprivate extension CustomTabView {
    @ViewBuilder
    func buttons() -> some View {
        ForEach(TabItem.allCases, id: \.self) { item in
            Button {
                if !item.text.isEmpty {
                    isSelectedItem = item 
                    // 텍스트가 있으면, 해당 탭을 눌렀을 때 화면 전환
                    // 텍스트 유무와 상관없이 화면전환을 하려면 if문 제거.
                }
                
                action(item: item)
            } label: {
                buttonLabel(item: item)
            }
            .frame(maxWidth: .infinity)
            .padding(.top, 12)
        }
    }
    
    @ViewBuilder
    func buttonLabel(item: TabItem) -> some View {
        if item.text.isEmpty { // text가 없을때, 이미지 사이즈 확장
            item.image
                .resizable()
                .renderingMode(.template)
                .scaledToFit()
                .foregroundStyle(.colors(.gray900))
                .frame(width: item.imageSize, height: item.imageSize)
        } else {
            VStack(spacing: 2) {  // text가 있을 때
                item.image
                    .resizable()
                    .renderingMode(.template)
                    .scaledToFit()
                    .foregroundStyle(isSelectedItem == item ? item.selectedColor : item.unsSelectedColor)
                    .frame(width: item.imageSize, height: item.imageSize)
                
                Text(item.text)
                    .pretendard(.medium12)
                    .foregroundStyle(isSelectedItem == item ? item.selectedColor : item.unsSelectedColor)
            }
        }
    }
    
    // MARK: - Methods
    func action(item: TabItem) { // 탭을 눌렀을 때의 추가 동작
        guard onTappedItem != nil else { return }
        
        onTappedItem!(item)
    }
}

 
 
 

사용 코드 예제

//
//  CustomTabView.swift
//
//  Created by Eddy on 2/19/24.
//

import SwiftUI


struct MainView: View {
    
    // MARK: - Properties
    @State private var isSelectedItem: TabItem = .left
    
    // MARK: - Views
    var body: some View {
        CustomTabView(isSelectedItem: $isSelectedItem) { item in
            switch item {
            case .left:
                EmptyView() // 왼쪽 뷰로 이동
            case .center:
                EmptyView() // 가운데 뷰로 이동
            case .right:
                EmptyView() // 오른쪽 뷰로 이동
            }
        } onTappedItem: { item in // (Optional) 버튼 동작
            switch item {
            case .left:
                print("왼쪽 버튼 동작")
            case .center:
                print("가운데 버튼 동작")
            case .right:
                print("오른쪽 버튼 동작")
            }
        }
    }
}

 
어쩌다보니 TabView를 직접 구현하게 되었는데, 의외로 굉장히 간단했습니다.
이정도라면 직접 CustomView를 구현해보기 좋은 레벨이라는 생각이 들었습니다.
SwiftUI에서는 이렇게 직접 구현해야하는 일이 은근히 많다는 점이, 까다롭기도 하고 재밌기도 하네요.

조금 더 디벨롭한다면, 특정 모델에 종속적인 View가 아닌 좀 더 범용성을 넣는다면 좋을 것 같았습니다. 프로젝트 개발 기간은 정해져있기에.. 일단은 이정도로 만족하게 되었네요. 앱에 TabView가 여러개 사용되는 앱은 많지 않으니까요.

반응형

댓글