🏃‍♀️

TCAバージョン1.4のマイグレーションまとめ

2023/11/24に公開

何があった?

  • TCAの1.4のアップデートが来ました(2023/11/22時点で1.4.2が最新リリース)
  • Macrosの対応が主な内容でマイグレーションガイドが公開されています
    • 本稿はこの記事の内容と同じです

Reducerマクロ

  • Reducerのプロトコルを外して@Reducerをつけるだけ

    +@Reducer
    -struct Counter: Reducer {
    +struct Counter {
    	// ...
    }
    
  • Reducerマクロ単体ではほぼReducerプロトコルと同じだけど、マクロでしか利用できない追加機能があります(以下に続く)

CaseKeyPathの導入

  • KeyPathでCasePathを記述できるようになった

    Reduce { state, action in 
      // ...
    }
    -.ifLet(\.child, action: /Action.child) {
    +.ifLet(\.child, action: \.child) {
      ChildFeature()
    }
    
  • 1.4~ではCasePathがsoft-deprecated

    • soft-deprecated: 将来的に廃止…くらいのやつ
  • Reducerマクロを適用したReducerでのみ利用できる

  • CasePathableマクロの導入によって、実行時のリフレクションの必要がなくなった

TestStoreの改善

  • CaseKeyPathによってTestStoreのreceiveメソッドで具体的なenumを記述する必要がなくなった

    -store.receive(.child(.presented(.response(.success("Hello!")))))
    +store.receive(\.child.presented.response.success)
    
  • PresentationActionであればさらに省略した記述ができる

    store.receive(\.child.response.success)
    
  • receiveで具体的なenumを受け取る(=比較する)必要がなくなったため、 Action: Equatable の必要がなくなった

    • if you use this style of action receiving exclusively という条件が書かれていたが、いまいちよくわからなかった
      • たぶんActionをEquatableで比較しなくなったため、receiveメソッドを同時に処理すると意図しない方に入るかもしれないよ、ってことかな?
  • ただしIdentifiedAction(後述)やStackActionを含むActionについては、subscript(id:)を用いることで具体的なenumを受け取ることができる

    store.receive(\.rows[id: 0].response.success)
    

TaskResultの廃止

  • Action: Equatableの必要がなくなり(上記TestStoreの改善を参照)、TaskResultの存在意義がおおよそなくなったためsoft-deprecatedとなった
    • TaskResultの主な役割は、Errorを含む型でありながらEquatableに準拠できることだった。これによってActionをEquatableに準拠させやすかった
  • 今後はSwiftの Result の使用を推奨

IdentifiedActionの導入

  • 今までListで表現されるような、複数存在しているUIからのActionは以下のように表現されていた

    enum Action {
      // ...
      case row(id: State.ID, action: Action)
    }
    
    // ...
    
    Reduce { state, action in 
      // ...
    }
    .forEach(\.rows, action: /Action.row(id:action:)) {
      RowFeature()
    }
    
  • IdentifiedAction によって以下のように書き換えられる。CaseKeyPathを利用しているのでReducerマクロを利用する必要がある

    enum Action {
      // ...
      case rows(IdentifiedActionOf<Nested>)
    }
    
    // ...
    
    Reduce { state, action in 
      // ...
    }
    .forEach(\.rows, action: \.rows) {
      RowFeature()
    }
    
  • 以前のように idaciton にアクセスしたい場合以下のような差分となる

    -case let .row(id: id, action: .buttonTapped):
    +case let .rows(.element(id: id, action: .buttonTapped)):
    

Reducerマクロのバグ(2023/11/22時点)

補完がバグる

  • ビルダーの型チェックとマクロの展開の順序の問題かもとのこと

  • Reducer内部の補完であれば書き方で解決できる

    var body: some Reducer<State, Action> {
    -  Reduce { state, action in
    +  Reduce<State, Action> { state, action in
    

循環参照エラー

  • 同じファイル内で適用されるマクロのコードに対して、拡張を追加することができない

    @Reducer
    struct Feature {
      struct State { /* ... */ }
      // ...
    }
    
    extension Feature.State {  // 🛑 循環参照エラー
      // ...
    }
    
  • Swiftとマクロのバグとのこと

  • 別のファイルに移すか、直接記述するしかないとのこと

まとめ

  • Macrosを適用して一部記法が変わった
  • ちょっとバグがある
  • swift 5.9~のみ利用可能
  • 使わない限りは関係なく、今まで通りの書き方もできる。がsoft-deprecatedなのでいつまでも放置はできない
  • ここで紹介していない変更点もあるので注意

Discussion