SwiftUI カスタマイズTabBar
リサーチ・アンド・イノベーションのhongです。iOSエンジニアをやっています。最近SwiftUIを触り始めました。メモを残したいので、Blogの初投稿をしようと思います。今回はSwiftUIでカスタマイズのTabBar作成について解説いたします。SwiftUIの自由度はとっても高いと思いますので、他の作り方もあると思います。
この記事の構成
この記事は3セッションに分けようと思います
セッション1
はSwiftUIが提供するTabBarを紹介します。
セッション2
は提供されたTabBarを使わずにViewでTabBarを作ります。
セッション3
はちょっとカスタマイズの要素を入れます。
セッション1 SwiftUIが提供するTabBar
とりあえず公式のTabBarをみてみましょう。
タブを二つ追加して、テキストとイメージを設置します。それぞれタップ可能です。
コードは以下となります。
TabBar.swift
import SwiftUI struct TabBar: View { var body: some View { TabView { HomeView() .tabItem { Image(systemName: "house") Text("ホーム") } CalendarView() .tabItem { Image(systemName: "calendar") Text("家計簿") } } } }
HomeView.swift
import SwiftUI struct HomeView: View { var body: some View { Text("ホーム") }
CalendarView.swift
import SwiftUI struct CalendarView: View { var body: some View { Text("家計簿") } }
ただの数行でこの様な効果が出ます。タップイベントは自動で設定されますし、UIKitと比べるととてもシンプルですね。 ちなみに使ったアイコンはSF Symbolsから探しました。
Appleが提供したTabBarはとても使いやすいですが、場合によって自分達でカスタマイズする必要あります。例えば真ん中のTabを大きくして、ユーザーにアピールしたい場合など。
例えば、弊社サービスCODEのアプリのTabBarはそうなっていたりします。
セッション2 Tab Barを作る
先ずは一個のタブのデータモデルを設計しましょう、タブが含む要素は多いですが、とりあえず重要な物から書きます:
- タイトル
- アイコン
- 識別子(タイトルを使ってもいいですが、あった方が見やすいかと)
最低限の要素を書いたので、モデルを書きましょう。以下となります:
Tab.swift
import SwiftUI enum Tab { case home case explore case review case calendar } struct TabItem: Identifiable { let id = UUID() let text: String let icon: String let tab: Tab } // sample data var tabItems = [ TabItem(text: "ホーム", icon: "house", tab: .home), TabItem(text: "検索", icon: "magnifyingglass", tab: .explore), TabItem(text: "評価", icon: "square.and.pencil.circle", tab: .review), TabItem(text: "家計簿", icon: "calendar", tab: .calendar) ]
モデルを作りました。Tabのenumを追加していて、これを識別子として使う予定です。一応後で使うサンプルデータも作りました。
モデル作成した後はいよいよSwift UIの出番ですね。とりあえずタップなしUIだけを作成します、いい感じに出来ています:
早速コードをみてみましょう:
TabBar.swift
struct TabBar: View { var body: some View { ZStack(alignment: .bottom) { ContentView() .frame(maxWidth: .infinity, maxHeight: .infinity) HStack { ForEach(tabItems) { item in VStack(spacing: 0) { Image(systemName: item.icon) .symbolVariant(.fill) .font(.body.bold()) .frame(width: 44, height: 29) Text(item.text) .font(.caption2) .lineLimit(1) } .frame(maxWidth: .infinity) } } .padding(.horizontal, 8) .padding(.top, 14) .frame(height: 88, alignment: .top) .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 34, style: .continuous)) .frame(maxHeight: .infinity, alignment: .bottom) .ignoresSafeArea() } } }
ZStack
の中にHStack
を入れてます。HStack
はよく使うかもしれませんが、ZStack
というものがあります。これはZ軸の概念があって、後から入れたビューが前に入れていたビューをブロックします。なので、最後に入れたHStack
はContentView
の上にいます、TabBarぽいですね。
最後にMaterial
指定のバックグラウンドを入れました。一行で綺麗なガラス効果を入れられるのは楽です。(ちなみにContentView
はただのオレンジビューでした)
UIは大体完成しました、あとはタップイベントを追加ですね。ボタンを入れて簡単にできると思います。
まず同じように完成した効果を見ましょう:
タップした際に画面が変更されてます。そして選択されたタブにはハイライトが付き、他のタブがグレーアウトします。
コードは以下となります
TabBar.swift
struct TabBar: View { // 選択されたタブのプロパティを追加 @State var selectedTab: Tab = .home var body: some View { ZStack(alignment: .bottom) { Group { // 選択したタブごとContentViewを切り替える switch selectedTab { case .home: ContentView() case .explore: ContentView2() case .review: ContentView3() case .calendar: ContentView4() } } .frame(maxWidth: .infinity, maxHeight: .infinity) HStack { ForEach(tabItems) { item in // VStackの外にButtonを追加して、ボタンのアクションにはitem.tabへの変更を設定します。そしてImageとTextをlabel:内に移動します Button { selectedTab = item.tab } label: { VStack(spacing: 0) { Image(systemName: item.icon) .symbolVariant(.fill) .font(.body.bold()) .frame(width: 44, height: 29) Text(item.text) .font(.caption2) .lineLimit(1) } .frame(maxWidth: .infinity) } .foregroundStyle(selectedTab == item.tab ? .primary : .secondary) } } .padding(.horizontal, 8) .padding(.top, 14) .frame(height: 88, alignment: .top) .background(.ultraThinMaterial) .frame(maxHeight: .infinity, alignment: .bottom) .ignoresSafeArea() } } }
コード内書いたコメント以外にもいくつか説明したい点があって:
- Group
について
- `selectedTab`の`switch`は`Group`内にいれています。`Group`内部全てのViewに同じmodifierを適用します。
Button
のforegroundStyle
について- secondaryとprimaryを使っています。secondaryの方はprimaryに比べて、わずかに透明で背後の色が少し透けて見えます。
item.tab
について、- 忘れたかもしれませんが、Tabのモデル作った時に入れてます^_^
自作のTabBarが大体完成しました。あとはカスタマイズの要素を入れましょう
セッション3 カスタマイズの要素を入れる
カスタマイズの要素を入れる点についてですが、そこまで珍しいものを入れるのではなく、とりあえずタブ上部に細いバーを追加しようと思います。
完成の効果をみてみましょう
タブの上に細いバーを入れました、アイコンだけの時より見やすいと思います。
では、コード側をみてみましょう
TabBar.swift
struct TabBar: View { @State var selectedTab: Tab = .home // 新しく追加されたプロパティです // バーのカラーとバーの長さです @State var color: Color = .black @State var tabItemWidth: CGFloat = 0 var body: some View { ZStack(alignment: .bottom) { // 前と同じなので省略します... HStack { ForEach(tabItems) { item in Button { selectedTab = item.tab } label: { VStack(spacing: 0) { Image(systemName: item.icon) .symbolVariant(.fill) .font(.body.bold()) .frame(width: 44, height: 29) Text(item.text) .font(.caption2) .lineLimit(1) } .frame(maxWidth: .infinity) } .foregroundStyle(selectedTab == item.tab ? .primary : .secondary) // overlayを置くと、buttonと同じframeの層を作ります .overlay( // GeometryReaderを使って buttonの長さを取得します GeometryReader { proxy in // ----- 参照1 ----- Color.clear.preference(key: TabWidthPreferenceKey.self, value: proxy.size.width) } ) .onPreferenceChange(TabWidthPreferenceKey.self) { value in tabItemWidth = value } } } .padding(.horizontal, 8) .padding(.top, 14) .frame(height: 88, alignment: .top) .background(.ultraThinMaterial) // overlayを使ってバーを一番上に置きます .overlay( overlay ) .frame(maxHeight: .infinity, alignment: .bottom) .ignoresSafeArea() } } var overlay: some View { HStack { // ----- 参照2 ----- if selectedTab == .calendar { Spacer() } if selectedTab == .explore { Spacer() } if selectedTab == .review { Spacer() Spacer() } Rectangle() .fill(color) .frame(width: 28, height: 5) .cornerRadius(3) .frame(width: tabItemWidth) .frame(maxHeight: .infinity, alignment: .top) if selectedTab == .home { Spacer() } if selectedTab == .explore { Spacer() Spacer() } if selectedTab == .review { Spacer() } } .padding(.horizontal, 8) } }
Tab.swift
import SwiftUI //... struct TabWidthPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } }
コード内に書いた 参照
コメント箇所について解説します:
- 参照1
について
- まずGeometryReader
を使ってますが、シンプルにproxy取得場所内にtabItemWidth = proxy.size.width
を書くとエラーが発生します。ここの値を取りたい場合、PreferenceKey
を使わないとダメです。Tab.swift内でTabWidthPreferenceKey
を追加しています。そうすると.preference
でproxy
内の値取り出すことが可能です。
- 取ったあと.onPreferenceChange
で値を保存できます。
参照2
について.overlay
で一番上にバーを置きます。バーの出る場所ですが、選択されたタブの上にしたいので、ここはSpacer
を使いました。ホームの場合右に一個Spacer
を入れます。検索の場合右二個左一個入れます。評価の場合は右一個左二個入れます。家計簿は左一個入れます。- Rectangleのframeを3回設定してます、一回目はバーのframeを設定(width: 28, height: 5)、二番目はバーを真ん中に表示するため先ほど取得した
tabItemWidth
をwitdhに設定します、そして三つ目はmaxHeight: .infinity
にして高さを最大にします、そうするとバーが一番上に表示されます。
切替が若干味気ないのでアニメーションをいれましょう。SwiftUIのアニメーションはとてもシンプルでわかりやすいです、selectedTab = item.tab
をwithAnimation
に入れるだけです。
TabBar.swift
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { selectedTab = item.tab }
完成しました、最後の効果をみてみましょう。ついでにLandscapeでも確認します。
問題なさそうですね。
まとめ
SwiftUIを実際にやるとやはりUIKitと大きい違いがありますね、でも慣れるとスピードも上がると思います。最も重要な点はxibとさよならできることだと思います。今後もSwiftUI移行を前向きに取り組んでいきます。
参考
リサーチ・アンド・イノベーションではエンジニアを募集中です。
興味を持っていただけた方はお気軽にご連絡ください!