The Composable Architecture(TCA)でアラート(Alert)を実装する方法

f:id:bamboohero:20210602153305p:plain

The Composable Architecture(TCA)のサンプルからアラート(Alert)の実装方法について学びます。

今回扱うサンプルはこちらです。

01-GettingStarted-AlertsAndActionSheets

「Alert」ボタンをタップするとアラートが表示され、アラートの中の「Increment」ボタンをタップするとCountの値が1増えるという機能が実装されています。

f:id:bamboohero:20210602070601g:plain


なお、同じサンプルコードにあるアクションシート(Action Sheet)の実装についてはこちらの記事で解説しています。合わせてご覧ください。

bamboo-hero.com



ネイティブ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」ボタンタップ時に何かしたい場合はこちらを実装することになると思います。


改めて全体の動きをまとめると以下のようになります。

  1. 画面上の「Alert」ボタンがタップされるとalertButtonTappedアクションが送信される
  2. Reducerによってstate.alertにAlertStateのインスタンスがセットされ、アラートが表示される
  3. アラート内の「Increment」ボタンがタップされるとincrementButtonTappedアクションが送信され、アラートは非表示になる
  4. Reducerによってstate.alertにAlertStateのインスタンスがセットされ、アラートが表示される。また、state.countの値が1増える
  5. アラート内の「OK」ボタンがタップされるとアラートが非表示になる。同時に、alertDismissedアクションが送信される
  6. Reducerによってstate.alertにnilがセットされる

アラートの実装においても、Actionを通じてReducerでStateが変更されるという単方向データフローの原則を守ることができています。


まとめ

TCAでアラートを実装する方法について説明しました。

ネイティブなSwiftUIで実装すると双方向のデータフローになりますが、TCAが提供してくれているアラートの拡張機能を使うことで単方向データフローの原則に従った実装をすることができることがわかりました。