SH Lab の アプリ開発部屋

リリースしたアプリの告知とかサポートとか、技術的なお話とか、そんな感じでつぶやきます。

SwiftUI 2.0 でカスタムタブビューを作ってみた

SwiftUI 2.0で追加されたAPI

SwiftUI 2.0 で追加されたPageTabViewStyleを使ってみたかったので、カスタムタブのようなものを作ってみました。

とりあえずの完成形

こういったものを作っていこうと思います。 999.gif

GitHubはこちらです。 github.com

開発環境

参考書

SwiftUIの勉強には、この辺りの本が特におすすめです!

基礎から学ぶ SwiftUI

基礎から学ぶ SwiftUI

事前準備

タブ部分の作成

スクリーンショット 2020-10-21 21.12.23.png

アニメーションgifファイルはこのように名前をつけて配置したので、名前を合わせる形でenumを定義しています。

enum TabItem: String, CaseIterable {
    case piyo
    case pen
    case neko
    case tobipen
    
    var name: String {
        "\(self.rawValue).gif"
    }
}

タブの一つ一つを表すためのTabItemViewを追加して、以下のように定義しました。

struct TabItemView: View {
    
    let tabItem: TabItem
    @Binding var selected: TabItem
    
    var body: some View {
        // SDWebImageSwiftUIのimportが必要.
        AnimatedImage(name: tabItem.name)
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 40)
            .onTapGesture {
                selected = tabItem // タップしたら自身をselectedに.
            }
    }
}

メインとなるContentViewには、以下のようにタブビューを定義しました。

struct ContentView: View {
    
    // タブの選択値と初期値.
    @State private var selected: TabItem = .piyo
    
    var body: some View {
        
        // タブビュー部分.
        HStack {
            ForEach(TabItem.allCases, id: \.self) { tabItem in
                TabItemView(tabItem: tabItem, selected: $selected)
            }
        }
        .padding(.vertical, 10.0)
        .padding(.horizontal, 20.0)
        .background(Color.white.clipShape(Capsule()))
        .shadow(color: Color.black.opacity(0.3), radius: 5, x: -5, y: 5)
    }
}

出来上がったのはこちらです。このままだと、選択状態がよくわからないですね。 001.gif

選択状態がわかるように見た目を調整する

選択時/非選択時で見た目を切り替えるため、TabItemViewを以下のように書き換えます。

  • frameを調整
  • paddingを調整
  • offsetを調整
  • タップ時の処理にアニメーションを伴わせる
var body: some View {
   AnimatedImage(name: tabItem.name)
       .resizable()
       .aspectRatio(contentMode: .fit)
       // 選択状態によって、サイズや間隔を調整する.
       .frame(width: tabItem == selected ? 100 : 40)
       .padding(.vertical, tabItem == selected ? -30 : 0)
       .padding(.horizontal, tabItem == selected ? -14 : 16)
       .offset(y: tabItem == selected ? -15 : 0)
       .onTapGesture {
           withAnimation(.spring()) {
               selected = tabItem // タップしたら自身をselectedに.
           }
       }
}

見た目はこのようになります。選択状態が一目でわかるようになりました。 002.gif

背景色の設定と、タブの配置調整

ContentViewの見た目を調整します。

  • 全体をZStackで囲う
  • 最背面にColor("bg").ignoresSafeArea()を配置して背景色とする
  • タブビュー部分をVStackSpacerを利用して画面下部に配置
var body: some View {
    
    ZStack {
        
        // 背景色.
        Color("bg").ignoresSafeArea()
        
        VStack {
            
            Spacer(minLength: 0)
            
            // タブビュー部分.
            HStack {
                ForEach(TabItem.allCases, id: \.self) { tabItem in
                    TabItemView(tabItem: tabItem, selected: $selected)
                }
            }
            .padding(.vertical, 10.0)
            .padding(.horizontal, 20.0)
            .background(Color.white.clipShape(Capsule()))
            .shadow(color: Color.black.opacity(0.3), radius: 5, x: -5, y: 5)
        }
    }
}

見た目はこうなりました。それっぽくなってきましたね。 003.gif

画面をタブで切り替える

タブは用意したので、このタブに連動して画面が切り替わるようにします。

まずはダミーで画面部分を用意します。
適当なので、こちらは好きに作ってもらって良いと思います。

struct HomeView: View {
    var body: some View {
        Text("Home")
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.red)
    }
}

struct ListView: View {
    var body: some View {
        Text("List")
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.green)
    }
}

struct SearchView: View {
    var body: some View {
        Text("Search")
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.blue)
    }
}

struct SettingView: View {
    var body: some View {
        Text("Setting")
            .font(.largeTitle)
            .fontWeight(.heavy)
            .foregroundColor(.yellow)
    }
}

最後に、これらのViewをTabViewで定義し、カスタムタブと連動するようにします。

  • TabViewの引数にselectedを指定することで、カスタムタブと連動させる
  • PageTabViewStyleを指定することで、横スワイプでの切り替えを可能にする
ZStack {
    
    // 背景色.
    Color("bg").ignoresSafeArea()
    
    // メイン画面部分はTabViewで定義.
    TabView(selection: $selected) {
        HomeView()
            .tag(TabItem.piyo)
        ListView()
            .tag(TabItem.pen)
        SearchView()
            .tag(TabItem.neko)
        SettingView()
            .tag(TabItem.tobipen)
    }
    // PageTabスタイルを利用する(インジケータは非表示).
    .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
    
    VStack {
        // 省略.
    }
}

まとめ

タブの定義がずいぶん簡単にできる印象ですが、それ以上に「切り替え用のUI」を簡単に作成できるのは嬉しいですね。

こちらの記事で作った切替ビューでも同じようなことができそうです。もしよかったら試してみてください。 hoshi0523.hatenablog.com