🌊

SwiftUI導入 - TabBar

2023/03/20に公開

この記事は?

SwiftUIを導入する話とそれを基に得た知見などをまとめていきます。

SwiftUIの発表から約3年が経ちそろそろSwiftUIを導入しなきゃな〜とお考えの皆さんの参考になったら幸いです!

https://zenn.dev/voicy/articles/be329b697fb5ff

TabBar

TabBarは、モバイルアプリの画面遷移に使われるビューの一種です。一般的に、アプリ内の主要な画面や機能を表すタブが配置されています。タブをタップすると、対応する画面が表示されます。

TabBarは、通常、画面下部に表示され、アプリのロゴやアイコンとともに表示されます。タブアイコンは、タップすると対応するビューが表示されます。TabBarは、多くの場合、アプリのユーザビリティを向上させるために使用されます。ユーザーは、アプリ内で自由に画面を切り替えることができ、アプリの機能やコンテンツを簡単に見つけることができます。

基本的なTabView

TabViewの実装はとてもシンプルです。
下記はよくあるTabViewの実装です。
Tabを選択した場合のコンテンツ、tabItemの定義、badge情報、これだけ一般的なTabViewの動作が実現できます。

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

            
            Text("Third Tab")
                .tabItem {
                    Image(systemName: "3.circle")
                    Text("Third")
                }
            .badge("New")
        }
    }
}

カスタムしたTabView

それでは少しカスタムを加えてみましょう
大きく下記2点の変更を加えてみました。

  • スワイプでのTab切り替え
  • Tabの一つをUIViewContlloerへ変更
struct CustomTabBarView: View {
    @State private var selectedTab = 0
    
    var body: some View {
        ZStack(alignment: .bottom) {
            TabView(selection: $selectedTab) {
                Text("First Tab Content")
                    .tag(0)
                
                Text("Second Tab Content")
                    .tag(1)
                
                UIViewControllerWrapper(uiViewController: ThirdViewController())
                    .tabItem {
                        Image(systemName: "person.fill")
                        Text("Profile")
                    }
                    .tag(2)
                    
            }
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
            .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
            .accentColor(.blue)
            
            
            TabBar(selectedTab: $selectedTab)
                .alignmentGuide(.bottom, computeValue: { dimension in
                    dimension[.bottom]
                })
        }.frame(width:375,height: 600)
    }
}




struct TabBar: View {
    @Binding var selectedTab: Int
    
    var body: some View {
        HStack {
            TabBarItem(imageName: "house.fill", text: "Home", tag: 0, selectedTab: $selectedTab)
            TabBarItem(imageName: "list.bullet", text: "List", tag: 1, selectedTab: $selectedTab)
            TabBarItem(imageName: "person.fill", text: "Profile", tag: 2, selectedTab: $selectedTab)
        }
        .background(Color.white)
        .padding()
    }
}

struct TabBarItem: View {
    let imageName: String
    let text: String
    let tag: Int
    @Binding var selectedTab: Int
    
    var body: some View {
        Button(action: {
            selectedTab = tag
        }) {
            VStack {
                  ZStack {
                      Image(systemName: imageName)
                          .font(.system(size: 24))
                      Badge(number: 1)
                          .offset(x: 10, y: -10)
                  }
                  Text(text)
                      .font(.footnote)
              }
              .foregroundColor(tag == selectedTab ? .blue : .gray)
              .padding(.vertical, 10)
              .frame(maxWidth: .infinity)
        }
    }
}

struct Badge: View {
    let number: Int
    
    var body: some View {
        Text("\(number)")
            .font(.system(size: 12))
            .foregroundColor(.white)
            .padding(.horizontal, 4)
            .padding(.vertical, 2)
            .background(Color.red)
            .clipShape(Circle())
    }
}

struct UIViewControllerWrapper: UIViewControllerRepresentable {
    let uiViewController: UIViewController
    
    func makeUIViewController(context: Context) -> UIViewController {
        return uiViewController
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
    }
}


class ThirdViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Add a label to the view controller's view
        let label = UILabel(frame: CGRect(x: view.bounds.width / 2, y: view.bounds.height / 2, width: 200, height: 50))
        label.center = view.center
        label.textAlignment = .center
        label.text = "Third Tab Content"
        
        view.translatesAutoresizingMaskIntoConstraints = false
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        NSLayoutConstraint.activate(
            [
                view.topAnchor.constraint(equalTo: label.topAnchor),
                view.bottomAnchor.constraint(equalTo: label.bottomAnchor),
                view.leadingAnchor.constraint(equalTo: label.leadingAnchor),
                view.trailingAnchor.constraint(equalTo: label.trailingAnchor)
            ]
        )
    }
}

ご覧の通りコード量がかなり多くなります。
特に tabViewStyle indexViewStyle Badge のカスタムをする必要性が出てきたりTabBarItemとTabViewの分離の連携が必要になります。このように少しカスタムするだけで無理な実装が入るので、SwiftUIの挙動を意識した画面設計が必要になります。

Voicyテックブログ

Discussion