The Composable Architecture(TCA)で画面遷移を実装する方法

f:id:bamboohero:20210601002137p:plain

The Composable Architecture(TCA)のサンプルから画面遷移の実装方法について学びます。

今回扱うサンプルは以下の3つです。



Stateがnilとnon-nilのときで画面表示を切り替える

画面遷移の実装に入る前に、Stateがnilとnon-nilなときに画面表示を切り替える方法について説明します。

01-GettingStarted-OptionalStateの画面は以下のようになっています。
「Toggle counter state」ボタンをタップすると、カウンタービューの表示・非表示が切り替わります。

f:id:bamboohero:20210531231616g:plain


この仕組みはIfLetStoreを使うことで実現されています。

struct OptionalBasicsState: Equatable {
  // [1]
  var optionalCounter: CounterState?
}

// [2]
IfLetStore(
  self.store.scope(
    state: \.optionalCounter,
    action: OptionalBasicsAction.optionalCounter
  ),
  then: { store in
    VStack(alignment: .leading, spacing: 16) {
      Text(template: "`CounterState` is non-`nil`", .body)
      CounterView(store: store)
        .buttonStyle(BorderlessButtonStyle())
    }
  },
  else: {
    Text(template: "`CounterState` is `nil`", .body)
  }
)

[1]
本サンプル画面のStateはCounterState?型のプロパティを持っています。

[2]
IfLetStoreにオプショナルなStateの型を持つStoreを渡します。ここではCounterState?型のStateを持つStoreを渡しています。
then:にはStoreが持つStateがnon-nilであるときに表示されるビューを指定します。
else:には反対にStateがnilであるときに表示されるビューを指定します。


IfLetStoreのドキュメントには、主に2つの用途で使用すると記載があります。

  1. 上記のようにStateが存在するかどうかでビューを切り替える
  2. 画面遷移時にビューを切り替える

2についてはこのあと見ていきます。


画面遷移したあとデータをロードする

03-Navigation-NavigateAndLoadの画面は以下のようになっています。
「Load optional countsaer」をタップすると画面遷移アクションが実行され、同時にデータのロードが実行されます。
1秒後にデータのロードが完了し、カウンタービューが表示されます。

f:id:bamboohero:20210531234315g:plain


実装で注目すべきはこのあたりです。

NavigationLink(
  // [1]
  destination: IfLetStore(
    self.store.scope(
      state: \.optionalCounter,
      action: NavigateAndLoadAction.optionalCounter
    ),
    then: CounterView.init(store:),
    else: { ActivityIndicator() }
  ),
  // [2]
  isActive: viewStore.binding(
    get: \.isNavigationActive,
    send: NavigateAndLoadAction.setNavigation(isActive:)
  )
) {
  HStack {
    Text("Load optional counter")
  }
}

// Reducerの実装
switch action {
  case .setNavigation(isActive: true):
    state.isNavigationActive = true
    return Effect(value: .setNavigationIsActiveDelayCompleted)
      .delay(for: 1, scheduler: environment.mainQueue)
      .eraseToEffect()

  case .setNavigation(isActive: false):
    state.isNavigationActive = false
    state.optionalCounter = nil
    return .none

  case .setNavigationIsActiveDelayCompleted:
    state.optionalCounter = CounterState()
    return .none

[1]
NavigationLinkのイニシャライザのdestination:引数にIfLetStoreが指定されています。
「Load optional counter」ボタンがタップされると画面遷移してdestination:に指定されたビューが表示されるのですが、IfLetStoreが指定されているため、optionalCounterがnilの間はelse:に指定されているインジケータが表示され、non-nilになったらthen:に指定されているCounterViewが表示されるという動きになります。

[2]
isActive:引数にはBindingを指定しています。
このBindingは次の画面への遷移が実行されると.setNavigation(isActive: true)を送信し、元の画面に戻る遷移が実行されると.setNavigation(isActive: false)を送信します。


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

  1. 「Load optional counter」ボタンがタップされると.setNavigation(isActive: true)が送信され、画面遷移する
  2. 画面遷移直後はまだoptionalCounterがnilであるため、インジケータが表示される
  3. 1秒後、setNavigationIsActiveDelayCompletedが送信され、optionalCounterがnon-nilになる
  4. optionalCounterがnon-nilになったので、CounterViewが表示される


データをロードしたあと画面遷移する

03-Navigation-LoadThenNavigateはデータをロードしてから画面遷移する例です。
「Load optional counter」ボタンをタップすると同時にデータのロードが開始され、インジケータが表示されます。
データのロードが完了したら画面遷移します。

f:id:bamboohero:20210601000954g:plain


実装で注目すべきはこのあたりです。

NavigationLink(
  // [1]
  destination: IfLetStore(
    self.store.scope(
      state: \.optionalCounter,
      action: LoadThenNavigateAction.optionalCounter
    ),
    then: CounterView.init(store:)
  ),
  isActive: viewStore.binding(
    get: \.isNavigationActive,
    send: LoadThenNavigateAction.setNavigation(isActive:)
  )
) {
  HStack {
    Text("Load optional counter")
    if viewStore.isActivityIndicatorVisible {
      Spacer()
      ActivityIndicator()
    }
  }
}

// Reducerの実装
switch action {
case .setNavigation(isActive: true):
  state.isActivityIndicatorVisible = true
  return Effect(value: .setNavigationIsActiveDelayCompleted)
    .delay(for: 1, scheduler: environment.mainQueue)
    .eraseToEffect()

case .setNavigation(isActive: false):
  state.optionalCounter = nil
  return .none

case .setNavigationIsActiveDelayCompleted:
  state.isActivityIndicatorVisible = false
  state.optionalCounter = CounterState()
  return .none

[1]
さきほどとの違いはIfLetStoreでelse:引数を指定していないことです。
こうすることで、optionalCounterがnilの間は画面遷移が実行されません。


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

  1. 「Load optional counter」ボタンがタップされると.setNavigation(isActive: true)が送信され、isActivityIndicatorVisibleがtrueになりインジケータが表示される
  2. 1秒後、setNavigationIsActiveDelayCompletedが送信され、optionalCounterがnon-nilになる。また、isActivityIndicatorVisibleがfalseになりインジケータが非表示になる
  3. optionalCounterがnon-nilになったので、画面遷移する


まとめ

オプショナルなStateでビューを切り替える方法と、画面遷移の実装方法について説明しました。

どれもアプリを実装する上でよく出てくるユースケースなので、今回学んだ内容は頻繁に実装していくことになると思います。