The Composable Architecture(TCA)でアラート(Alert)を実装する方法
The Composable Architecture(TCA)のサンプルからアラート(Alert)の実装方法について学びます。
今回扱うサンプルはこちらです。
01-GettingStarted-AlertsAndActionSheets
「Alert」ボタンをタップするとアラートが表示され、アラートの中の「Increment」ボタンをタップするとCountの値が1増えるという機能が実装されています。
なお、同じサンプルコードにあるアクションシート(Action Sheet)の実装についてはこちらの記事で解説しています。合わせてご覧ください。
ネイティブSwiftUIでのアラート実装例
同じ機能をTCAを使わずネイティブなSwiftUIで実装するとこんな感じになると思います。
struct ContentView: View { @State private var count = 0 @State private var showAlert = false var body: some View { Form { Text("Count: \(count)") Button("Alert") { showAlert = true } // [1] .alert(isPresented: $showAlert) { Alert( title: Text("Alert!"), message: Text("This is an alert"), primaryButton: .cancel( Text("Cancel"), action: {} ), secondaryButton: .default( Text("Increment"), action: increment ) ) } } } private func increment() { count += 1 } }
[1]
「Alert」ボタンをタップするとshowAlert
がtrueになり、アラートが表示されます。
アラートのいずれかのボタンをタップしてアラートが非表示になると、Bindingを通じてshowAlert
がfalseになります。
SwiftUIではこのように双方向バインディングを使用してアラートの表示・非表示をコントロールしていますが、TCAは単方向データフローが原則です。
このため、TCAはアラートの実装で単方向データフローを守るための仕組みを用意しています。
TCAでのアラート実装例
サンプルからアラートを表示するビューの実装だけを抜き出したのがこちらです。
// [1] Button("Alert") { viewStore.send(.alertButtonTapped) } // [2] .alert( self.store.scope(state: \.alert), dismiss: .alertDismissed )
[1]
ボタンがタップされたら.alertButtonTapped
アクションを送信します。
[2]
alert(_:dismiss:)
はTCAが提供するViewのエクステンションメソッドです。
メソッドのシグネチャは以下のようになっています。AlertStateというのはTCAが提供する構造体で、Storeが持っているAlertStateがnon-nilになるとアラートが表示されます。
アラートのボタンをタップするなどしてアラートが非表示になると、dismiss:
のアクションが送信されます。
func alert<Action>(_ store: Store<AlertState<Action>?, Action>, dismiss: Action) -> some View
このように、TCAのアラート実装を利用することでボタンタップやアラートの非表示時にアクションを送信し、ReducerでStateを変更するという単方向データフローの原則に従うことができます。
続いて、State、Reducerの実装を見ていきます(サンプルにはアクションシートの実装も含まれていますが、アラートに関連する部分だけ抜き出します)。
struct AlertAndSheetState: Equatable { // [1] var alert: AlertState<AlertAndSheetAction>? var count = 0 } let alertAndSheetReducer = Reducer< AlertAndSheetState, AlertAndSheetAction, AlertAndSheetEnvironment > { state, action, _ in switch action { // [2] case .alertButtonTapped: state.alert = .init( title: .init("Alert!"), message: .init("This is an alert"), primaryButton: .cancel(), // [3] secondaryButton: .default(.init("Increment"), send: .incrementButtonTapped) ) return .none // [4] case .incrementButtonTapped: state.alert = .init(title: .init("Incremented!")) state.count += 1 return .none } // [5] case .alertDismissed: state.alert = nil return .none // [6] case .alertCancelTapped: return .none }
[1]
AlertStateのプロパティはオプショナルで宣言しておきます。先ほど説明したように、このプロパティにAlertStateのインスタンスをセットすると、アラートが表示されるようになります。
[2]
画面上の「Alert」ボタンをタップするとalertButtonTapped
アクションが送信され、Reducerがstate.alert
にAlertStateのインスタンスをセットします。これでアラートが画面に表示されます。
[3]
アラート内の「Increment」ボタンをタップするとincrementButtonTapped
アクションが送信されます。
[4]
incrementButtonTapped
アクションが送信されたら、Reducerがstate.alert
に新しいAlertStateのインスタンスをセットします。これで「Incremented!」というタイトルの新しいアラートが画面に表示されます。
[5]
アラートのいずれかのボタンがタップされアラートが非表示になるとalertDismissed
が送信され、Reducerでstate.alert
をnilに変更します。
ここでstate.alert
をnilにしておかないと、もう一度「Alert」ボタンをタップした直後にalertDismissed
アクションが送信され、いずれかのボタンをタップしてアラートが非表示になると再度alertDismissed
アクションが送信されるという動きをしました。
深く追えてはいないのですが、想定外の動きにならないようにnilにしておくのが良いでしょう。
[6]
アクションは定義されていますが、このアクションはどこからも送信されていないようです。アラート内の「Cancel」ボタンタップ時に何かしたい場合はこちらを実装することになると思います。
改めて全体の動きをまとめると以下のようになります。
- 画面上の「Alert」ボタンがタップされると
alertButtonTapped
アクションが送信される - Reducerによって
state.alert
にAlertStateのインスタンスがセットされ、アラートが表示される - アラート内の「Increment」ボタンがタップされると
incrementButtonTapped
アクションが送信され、アラートは非表示になる - Reducerによって
state.alert
にAlertStateのインスタンスがセットされ、アラートが表示される。また、state.count
の値が1増える - アラート内の「OK」ボタンがタップされるとアラートが非表示になる。同時に、
alertDismissed
アクションが送信される - Reducerによって
state.alert
にnilがセットされる
アラートの実装においても、Actionを通じてReducerでStateが変更されるという単方向データフローの原則を守ることができています。
まとめ
TCAでアラートを実装する方法について説明しました。
ネイティブなSwiftUIで実装すると双方向のデータフローになりますが、TCAが提供してくれているアラートの拡張機能を使うことで単方向データフローの原則に従った実装をすることができることがわかりました。