😬

[TCA] TCAのTree-based Navigationのコードを理解する

2024/09/01に公開

概要

TCAのTree-based Navigationにでてくる以下の要素を理解する。

  • @Presents
  • PresentationAction
  • ifLet(_:action:destination:fileID:filePath:line:column:)

TCAのTree-based Navigationってなんだっけ?

https://zenn.dev/ueeek/articles/20240901tca_navigation

Tree-based Navigationを使用した実装例

"商品のリストを表示するView"(ItemListFeature)と、その画面からDrill-downで遷移する"商品詳細View"(ItemDetailFeature)があったとする。

TCAのTree-based Navigationを利用すると、以下のようなコードになる。

ItemListFeatureReducer.swift
@Reducer
struct ItemListFeature {
    @ObservableState
    struct State: Equatable {
        @Presents var itemDetail: ItemDetailFeature.State?
        // ...
    }

    enum Action {
        case itemDetail:(PresentationAction<ItemDetailFeature.Action>)
        // ...
    }

    var body: some ReducerOf<Self> {
        Reduce { state, action in 
            // ...
        }
        .ifLet(\.$itemDetail, action: \.itemDetail) {
            ItemDetailFeature()
        }
    }
ItemListView.swift
struct ItemListView: View {
    @Bindable var store: StoreOf<ItemDetailFeature>

    var body: some View {
        ListView()
        .navigationDestination(item: $store.scope(state: \.itemDetail, action: \.itemDetail) { store in 
            ItemDetailView(store: store)
        }
    }
}

コードには、@PresentsPresentationActionifLet(, action:)などがでてくる。

@Presents

@ObservableState
struct State: Equatable {
    @Presents var itemDetail: ItemDetailFeature.State?
    // ...
}
  • PresentationStateObservableStateを組み合わせて使うためのMacro
  • PresentationStateは、PresentされるStateのためのProperty Wrapper
  • 後述のifLetと組み合わせて使用する。

PresentationAction

enum Action {
        case itemDetail:(PresentationAction<ItemDetailFeature.Action>)
        // ...
    }
  • PresentされるActionのためのProperty Wrapper
  • 後述のifLetと組み合わせて使用する。
  • ChildFeatureが表示・非表示されるときに呼ばれる2つのActionを提供する
    • PresentationAction.presented(_:)
      • ChildFeature内部でActionが発行されたときに呼ばれる。
    • PresentationAction.dismiss
      • PresentationStateにnilがセットされたときに呼ばれる。
      • ChildFeatureStatenilになる前に、ParentFeatureからChildFeatureStateにアクセスできる。

PresentationAction.presented(_:), PresentationAction.dismissの使用例

ItemListFeature.swift
    // ...
    var body: some ReducerOf<Self> {
        Reduce { state, action in 
            switch action {
                //...
                case .itemDetail(.presented(.addItem)):
                    //...
                case .itemDetail(.dismiss):
                    //...
            }
        }
        .ifLet(\.$itemDetail, action: \.itemDetail) {
            ItemDetailFeature()
        }
    }
  • .itemDetail(.presented(.addItem)):は、子ViewであるItemDetailFeatureにおいて、ItemDetailFeature.Action.addItemが発行されたときに呼ばれる。
  • .itemDetail(.dismiss):は、親Stateの中のitemDetail Stateにnilがセットされると呼ばれる。

ifLet(_:action:destination:fileID:filePath:line:column:)

Reduce { state, action in 
}
.ifLet(\.$itemDetail, action: \.itemDetail) {
    ItemDetailFeature()
}
  • パラメータ
    • toPresentationState
      • 第一引数でPresentしたいChildFeatureのStateを受け取る
      • Stateの型は、PresentationStateへのKeyPathになっている。(ChildStateが@Presentである必要)
    • toPresentationAction
      • 第二引数で、PresentしたChildFeatureのActionを受け取る
      • Actionの型は、PresentationActionへのCasePathになっている(ChildActionがPresentationActionである必要)。
    • destination
      • 第三引数で、PresentしたいChildFeatureのReducerを受け取る
  • 戻り値
    • ParentFeatureのReducerと、PresentしたいChildFeatureのReducerを、組み合わせたReducer
  • ifLetのはたらき
    • PresentationAction.dismissのActionが呼ばれたときは、ChildStateをnilにする前にParentFeatureが呼ばれる。(nilになる前に、ChildFeature.StateにParentがアクセスできる)
    • PresentationAction.presented(_:)は、ChildFeature → ParentFeatureの順で呼ばれる。
    • ChildStateがnilになったときは、ChildFeatureのEffectを自動でキャンセルする
    • ChildFeatureから自身をdismissできるようにする。

Envrionment

  • TCA v1.13.0

SampleCode



https://github.com/Ueeek/TCATreeBasedNavigationSample/blob/main/TCATreeBasedNavigationSample/Features/ItemList/ItemListFeature1.swift

Ref

Discussion