【SwiftUI】TabViewのタブが選択された時に処理を実行する

2022.12.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

SwiftUIのTabViewはとても簡単に実装出来て嬉しいですが、そのTabViewのタブが選択された時に処理を実行したかったので調べました。

TabViewを使う

TabViewContentにコンテンツとして表示させた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

タブをクリックした際に処理を実行する

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
                }
            }
        }
    }
}

プレビュー

tabView2

無事にタブを選択した際に、処理を実行させることが出来ました。

ただonChangeを使用する方法だとEquatableの値がイコールではない時にしかonChangeは呼ばれない為、選択されているタブをクリックしても何もアニメーションは実行されません。

tabView3

スマイルのタブが選択中もスマイルのタブをクリックしていますが、アニメーションが実行されません。

選択中のタブをクリックしても処理を実行する

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()
        }
    }
}

onChageonReceiveに変更して、パブリッシャーからデータが発行された場合にアニメーション処理を実行するようにしています。

プレビュー

選択済みのスマイルタブをクリックしてもアニメーションが実行されるようになりました。

tabView4

ただし、この方法だとView生成時にもonReceiveが呼ばれてしまう為、初回は実行させたくないという思いがある場合は制御が必要になります。

Bindingのエクステンションを作成する方法

上記はonReceiveを使用する方法でしたが、Bindingのエクステンションメソッドを作成しても実現出来ます。

extension Binding {
    func onUpdate(_ closure: @escaping () -> Void) -> Binding<Value> {
        Binding(
            get: {
                wrappedValue
            },
            set: { newValue in
                wrappedValue = newValue
                closure()
            }
        )
    }
}

これはバインディングで新しい値が渡って来る度にクロージャー処理を実行できるメソッドになります。

使い方はTabViewselectionに渡している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のタブが選択された時に処理を実行できるようになりました。これによって、特定のタブが選択された時は画面の遷移を元に戻したい等が実装できるようになり良かったです。

この記事が誰かの助けになれば嬉しいです。

参考