Swift/UIKit 개발 노트

[iOS/SwiftUI] TabView

힛해 2024. 8. 22. 01:07
728x90

https://developer.apple.com/documentation/swiftui/tabview

 

TabView | Apple Developer Documentation

A view that switches between multiple child views using interactive user interface elements.

developer.apple.com

 

프로젝트들을 진행하며 디자이너분의 요구사항을 충족하기위해 일반적으로 하단 탭바를 커스텀으로 구현하지만

기본으로 제공하는 UI 구조체 요소를 알아두면 개발 속도도 빨라지고 틀을 우선적으로 구현하고 와이어프레임을 팀원에게 공유할 수 있어서 좋기에 TabView의 사용법에 대해 공부해보자.

 

@MainActor @preconcurrency
struct TabView<SelectionValue, Content> where SelectionValue : Hashable, Content : View

 

공식문서에 나와있는 사용 방법들을 차례대로 따라해보자.

 

1. Badge가 있는 탭뷰

TabView {
    Tab("Received", systemImage: "tray.and.arrow.down.fill") {
        ReceivedView()
    }
    .badge(2)


    Tab("Sent", systemImage: "tray.and.arrow.up.fill") {
        SentView()
    }


    Tab("Account", systemImage: "person.crop.circle.fill") {
        AccountView()
    }
    .badge("!")
}

 

어라?? Tab을 찾을 수 없다고 실행되지 않는다.

 

Tab 문서에 들어가보자.

https://developer.apple.com/documentation/swiftui/tab

 

Tab | Apple Developer Documentation

The content for a tab and the tab’s associated tab item in a tab view.

developer.apple.com

 

아아 ios 18 이상에 적용되는 거였다. ( 아니 베타버전인데 이걸 왜 공식문서에 벌써...? 누군가 이전 버전 공식문서를 가지고있거나 접근 방법을 알고 있다면 댓글 남겨주길 바랍니다.. )

 

1. 기본적인 탭뷰

공식문서가 아닌 인터넷 서칭을 통해 알아보았다.

 

struct ContentView: View {
    var body: some View {
        TabView {
            Text("First Tab")
                .tabItem {
                    Image(systemName: "1.circle")
                    Text("First")
                }.badge(1)
            
            Text("Second Tab")
                .tabItem {
                    Image(systemName: "2.circle")
                    Text("Second")
                }
            
            Text("Third Tab")
                .tabItem {
                    Image(systemName: "3.circle")
                    Text("Third")
                }.badge("!")
        }
    }
}
}

 

TabView 안에 보여주길 원하는 컴포넌트들을 묶지않고 순서대로 선언을 해주면 아래와 같은 화면이 만들어지게 된다.

 

2. PageTabViewStyle()

 

 

 

공식문서로 알아보기에는 내용도 자유도도 매우 적은 UI요소였다.

 

ios 18에 적용되는 Tab을 보니 디자인도 커스텀정의요소도 많아 보였지만 아직 17.5를 사용하고있는 현재 시점에서는 너무 먼 미래같다.

 

일반적으로 사용할때 공통적인 특징이 있었다.

 

  1. NavigationStack으로 뷰 감싸주기
  2. enum이나 Int로 tag 구성
  3. ZStack을 활용하여 커스텀 버튼 구현

1. NavigationStack

struct ContentView: View {
    var body: some View {
        TabView {
            NavigationStack() {
                Text("First Tab")
            }
                .tabItem {
                    Image(systemName: "1.circle")
                    Text("First")
                }.badge(1)
            
            NavigationStack() {
                Text("Second Tab")
            }
                .tabItem {
                    Image(systemName: "2.circle")
                    Text("Second")
                }
            NavigationStack() {
                Text("Third Tab")
            }
                .tabItem {
                    Image(systemName: "3.circle")
                    Text("Third")
                }.badge("!")
        }.tabViewStyle(DefaultTabViewStyle())
    }
}

 

Text를 바로 TabView 하위에 넣기보다 NavigationStack에 감싸서 넣어준다.

 

이유는 다음과 같다.

  • 독립적인 탐색 관리: 각 탭은 서로 다른 탐색 흐름을 가질 수 있다.
  • 탭 간 상태 독립성: NavigationStack을 각 탭에 넣으면 탭 간의 탐색 상태가 독립적으로 유지되는데 사용자가 한 탭에서 화면을 여러 번 전환한 후 다른 탭으로 이동해도, 원래 탭으로 돌아왔을 때 이전 탐색 상태가 유지된다.
  • UI 일관성: iOS 디자인 가이드라인에 따르면, 탭 내에서의 화면 전환은 일반적으로 내비게이션 구조를 따라야 하는데 NavigationStack은 이러한 UI 패턴을 쉽게 구현할 수 있다.

 

2. tag 선언

크게 enum을 사용해서 사용자 정의 태그를 만들어주는 방식과 Int로 selection을 관리해주는 방법이 있다.

 

1. enum

struct ContentView: View {
    enum Tab { 
        case a, b, c
    }
    
    @State private var selected: Tab = .a
    
    var body: some View {
        ZStack {
                    TabView(selection: $selected) {
                        Group {
                            NavigationStack {
                                TmpView(name: 1 )
                            }
                            .tag(Tab.a)
                            
                            NavigationStack {
                                TmpView(name: 2 )
                            }
                            .tag(Tab.b)
                            
                            NavigationStack {
                                TmpView(name: 3 )
                            }
                            .tag(Tab.c)
                        }
                        .toolbar(.hidden, for: .tabBar)
                    }
                    
                }
    }

 

 

2. Int

 @State  private  var selectedIndex: Int  =  0
 
 TabView (selection : $selctedIndex) {
    NavigationStack () { 
                Text ( "홈 뷰" ) 
                    .navigationTitle( "홈" ) 
            } 
            .tabItem { 
                Text ( "홈 뷰" ) 
                Image (systemName: "house.fill" ) 
                    .renderingMode(.template) 
            } 
            .tag( 0 ) 
            
            NavigationStack () { 
                Text ( "프로필 뷰" ) 
                    .navigationTitle( "프로필" ) 
            } 
            .tabItem { 
                Label ( "프로필" , systemImage: "person.fill" ) 
            } 
            .tag( 1 ) 
 }

 

 

3. ZStack 을 활용한 커스텀 탭바 구현

커스텀 탭바 구현은 이분의 블로그를 참고했다.

 

SwiftUI - TabView Custom TabBar

앱을 실행했을 때 이정표처럼 사용자가 가야 할 곳을 알려주고, 내 앱의 분위기를 확실하게 전달하는 수단으로 이 TabView는 최근 다양하고 예쁜 스타일로 고리타분함에서 변화를 꾀하고 있다.

velog.io

 

import SwiftUI


struct ContentView: View {
    enum Tab { // Tag에서 사용할 Tab 열겨형
        case a, b, c
    }
    
    @State private var selected: Tab = .a // 선택된 Tab을 컨트롤할 수 있는 상태 변수
    
    var body: some View {
        ZStack {
                    TabView(selection: $selected) {
                        Group {
                            NavigationStack {
                                TmpView(name: 1 )
                            }
                            .tag(Tab.a)
                            
                            NavigationStack {
                                TmpView(name: 2 )
                            }
                            .tag(Tab.b)
                            
                            NavigationStack {
                                TmpView(name: 3 )
                            }
                            .tag(Tab.c)
                        }
                        .toolbar(.hidden, for: .tabBar)
                    }
                    
                    VStack {
                        Spacer()
                        tabBar
                    }
                }
    }
    
    var tabBar: some View {
        HStack {
            Spacer()
            
            Button {
                selected = .a
            } label: {
                VStack(alignment: .center) {
                    Image(systemName: "star")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 22)
                    if selected == .a {
                        Text("View A")
                            .font(.system(size: 11))
                    }
                }
            }
            .foregroundStyle(selected == .a ? Color.green : Color.blue)
            
            Spacer()
            
            Button {
                selected = .b
            } label: {
                VStack(alignment: .center) {
                    Image(systemName: "gauge.with.dots.needle.bottom.0percent")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 22)
                    if selected == .b {
                        Text("View B")
                            .font(.system(size: 11))
                    }
                }
            }
            .foregroundStyle(selected == .b ? Color.green : Color.blue)
            Spacer()
            
            Button {
                selected = .c
            } label: {
                VStack(alignment: .center) {
                    Image(systemName: "fuelpump")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 22)
                    if selected == .c {
                        Text("View C")
                            .font(.system(size: 11))
                            .badge("!")
                    }
                }
            }
            .foregroundStyle(selected == .c ? Color.green : Color.blue)
            
            Spacer()
        }
        .padding()
        .frame(height: 72)
        .background {
            RoundedRectangle(cornerRadius: 24)
                .fill(Color.white)
                .shadow(color: .black.opacity(0.15), radius: 8, y: 2)
        }
        .padding(.horizontal)
    }

}

struct TmpView : View {
    var name : Int
    
    var body : some View {
        VStack{
            Text("탭 : \(name)")
        }
        .edgesIgnoringSafeArea(.all)
        .frame(width: 500, height: 500)
        .background(Color.green)
    }
}

 

정리하자면

확실히 기본적으로 제공해주는 TabView는 너무 제한적이고 앱의 분위기에 맞추었을때 탭바를 변경하는게 디자인적으로도 훌륭하기에 기본 스타일을 사용하지는 않을 것 같다.

 

하지만 쉽게 구현하기위해서 TabView를 활용하고 위와같은 방법으로 사용하면 개발속도는 물론 일관성있는 개발이 가능할 것 같다.