SwiftUIのTabView
はとても簡単に実装出来て嬉しいですが、そのTabView
のタブが選択された時に処理を実行したかったので調べました。
TabViewを使う
TabView
のContent
にコンテンツとして表示させたViewを記述することで使用出来ます。その際は、.tag
に一意の値を渡すことで特定のタブへの切り替えを可能にします。
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
ForEach(SelectionType.allCases) { type in
Image(systemName: type.rawValue)
.font(.system(size: 300))
.tag(type)
.tabItem {
Image(systemName: type.rawValue)
}
}
}
}
}
extension ContentView {
enum SelectionType: String, Identifiable, CaseIterable {
case circle = "circle"
case smile = "face.smiling"
case peaceSign = "peacesign"
var id: String {
return rawValue
}
}
}
プレビュー
タブをクリックした際に処理を実行する
TabView
のタブが選択された時に何らかの処理を実行したい時は、選択されている値を保持するプロパティSelectionValue
を用意して、onChange
で値の変更がある度に処理を実行する方法があります。
struct ContentView: View {
/// 選択されている値
@State private var selectionType: SelectionType = .circle
var body: some View {
TabView(selection: $selectionType) {
ForEach(SelectionType.allCases) { type in
Image(systemName: type.rawValue)
.font(.system(size: 300))
.tag(type)
.tabItem {
Image(systemName: type.rawValue)
}
}
}
.onChange(of: selectionType) { _ in
print("tab changed into", selectionType)
}
}
}
デモ
print
出力だと動きが分かりにくい為、今回は例として、選択されたタブのView
を拡大させて元に戻るようなアニメーションを実行させてみます。
アニメーションの部分のついては今回の内容の趣旨とは違う為、説明は省略させていただきます。
import SwiftUI
struct ContentView: View {
@State private var selectionType: SelectionType = .circle
/// アニメーション実行用フラグ
@State private var shouldStartAnimation = false
var body: some View {
TabView(selection: $selectionType) {
ForEach(SelectionType.allCases) { type in
Image(systemName: type.rawValue)
.font(.system(size: 300))
.tag(type)
.tabItem {
Image(systemName: type.rawValue)
}
// アニメーションフラグによってscaleを変更
.scaleEffect(shouldStartAnimation ? 1.5 : 1.0)
}
}
.onChange(of: selectionType) { _ in
// アニメーションを実行
withAnimation {
shouldStartAnimation = true
}
// 0.5秒後にアニメーションを終了
Timer.scheduledTimer(withTimeInterval: 0.5,
repeats: false) { _ in
withAnimation {
shouldStartAnimation = false
}
}
}
}
}
プレビュー
無事にタブを選択した際に、処理を実行させることが出来ました。
ただonChange
を使用する方法だとEquatable
の値がイコールではない時にしかonChange
は呼ばれない為、選択されているタブをクリックしても何もアニメーションは実行されません。
スマイルのタブが選択中もスマイルのタブをクリックしていますが、アニメーションが実行されません。
選択中のタブをクリックしても処理を実行する
onChange
の代わりにonReceive
を使用して特定のパブリッシャーから発行されたデータを検出した時に処理を実行するようにします。
今回はViewModel
を作成し、各@State
プロパティを@Publiched
を付与したプロパティに変更しました。
ViewModel
class ViewModel: ObservableObject {
@Published var selectionType: SelectionType = .circle
@Published var shouldStartAnimation = false
var scaleEffectValue: CGFloat {
return shouldStartAnimation ? 1.5 : 1.0
}
func startAnimation() {
withAnimation {
shouldStartAnimation = true
}
Timer.scheduledTimer(withTimeInterval: 0.5,
repeats: false) { _ in
withAnimation {
self.shouldStartAnimation = false
}
}
}
}
プロパティ以外にもアニメーションを実行する処理もViewModel
側に記述しました。
ContentView
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
TabView(selection: $viewModel.selectionType) {
ForEach(SelectionType.allCases) { type in
Image(systemName: type.rawValue)
.font(.system(size: 300))
.tag(type)
.tabItem {
Image(systemName: type.rawValue)
}
.scaleEffect(viewModel.scaleEffectValue)
}
}
.onReceive(viewModel.$selectionType) { _ in
viewModel.startAnimation()
}
}
}
onChage
をonReceive
に変更して、パブリッシャーからデータが発行された場合にアニメーション処理を実行するようにしています。
プレビュー
選択済みのスマイルタブをクリックしてもアニメーションが実行されるようになりました。
ただし、この方法だとView生成時にもonReceive
が呼ばれてしまう為、初回は実行させたくないという思いがある場合は制御が必要になります。
Bindingのエクステンションを作成する方法
上記はonReceive
を使用する方法でしたが、Binding
のエクステンションメソッドを作成しても実現出来ます。
extension Binding {
func onUpdate(_ closure: @escaping () -> Void) -> Binding<Value> {
Binding(
get: {
wrappedValue
},
set: { newValue in
wrappedValue = newValue
closure()
}
)
}
}
これはバインディングで新しい値が渡って来る度にクロージャー処理を実行できるメソッドになります。
使い方はTabView
のselection
に渡しているSelectionValue
の箇所でonUpdate
を記述し、クロージャー内に実行したい処理を記述します。
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
TabView(selection: $viewModel.selectionType.onUpdate {
viewModel.startAnimation()
}) {
ForEach(SelectionType.allCases) { type in
Image(systemName: type.rawValue)
.font(.system(size: 300))
.tag(type)
.tabItem {
Image(systemName: type.rawValue)
}
.scaleEffect(viewModel.scaleEffectValue)
}
}
}
}
この方法だとView生成時にはアニメーションは実行されず、タブをクリックした時のみにアニメーションが呼ばれるようになりました。
おわりに
無事にTabView
のタブが選択された時に処理を実行できるようになりました。これによって、特定のタブが選択された時は画面の遷移を元に戻したい等が実装できるようになり良かったです。
この記事が誰かの助けになれば嬉しいです。