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