LoginSignup
4
1

[TCA] ReducerからViewをdismissする方法

Last updated at Posted at 2023-05-31

Reducerからdismissする方法を公式チュートリアルから発見したので、今回はご紹介します。
また、この機能はTCAのv0.54.0からなので、ご注意ください。

実装

struct HogeReducer: ReducerProtocol {
    struct State: Equatable {}
    enum Action: Equatable {
        case someAction
    }

    @Dependency(\.dismiss) var dismiss
    
    func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
        switch action {
        case .someAction:
           return .run { _ in
                await dismiss()
           }
        }
    }
}

Depenedencyでdismissを指定することでReducerからdismissできるようです。

また、公式ドキュメントにはこのように注意書きがあります。

The @Dependency(.dismiss) tool only works for features that are presented using the ifLet operator for tree-based navigation (see Tree-based navigation for more info) or forEach operator for stack-based navigation (see Stack-based navigation). If no parent feature is found that was presented with ifLet or forEach, then a runtime warning is emitted in Xcode letting you know that it is not possible to dismiss. Further, the runtime warning becomes a test failure when run in tests.

If you are testing a child feature in isolation that makes use of @Dependency(.dismiss) then you will need to override the dependency to get a passing test. You can even mutate some shared mutable state inside the dismiss closure to confirm that it is indeed invoked:

そのままDeepLにぶち込んでみると、

依存関係(@Dependency)ツールは、ツリーベースのナビゲーションの ifLet オペレータ(詳細はツリーベースのナビゲーションを参照)またはスタックベースのナビゲーションの forEach オペレータ(スタックベースのナビゲーションを参照)を使用して提示される機能に対してのみ動作します。ifLet または forEach で提示された親フィーチャーが見つからない場合、Xcode で実行時警告が発せられ、解除できないことを知らせます。さらに、この実行時警告は、テストで実行するとテストの失敗となる。

もし、@Dependency(.dismiss) を利用した子機能を単独でテストする場合、テストに合格するためには、依存関係をオーバーライドする必要があります。dismissクロージャの内部で共有されたミュータブルな状態を変異させて、それが本当に呼び出されたことを確認することもできます:

つまり、@ Dependencyでのdismissを使用するには、TCAのv0.54.0で追加されたPresentationStateやStackStateを使用して画面遷移を行った場合に限るっぽいです。もしそのような実装がなかったらランタイムエラーが出るとのこと。
また、@ Dependencyでのdismissを使用した場合のテストについても記述があり、dismissクロージャを任意の処理にオーバーライドすることでテストを成功させることができるそうです。今回の場合はこのようにテストを記述します。

let isDismissInvoked = LockIsolated(false)
let store = Store(initialState: HogeReducer.State()) {
  HogeReducer()
} withDependencies: {
  $0.dismiss = { isDismissInvoked.setValue(true) }
}

await store.send(.someAction)
XCTAssertEqual(isDismissInvoked.value, true)

dismissクロージャはSendableなので、Sendableに準拠していて内部でロック機構を持つLockIsolatedで値をWrapしてあげ、dismissが呼ばれた時にtrueをsetしています。
someActionでdismissを呼んでいるので、someActionをsendした時にisDismissInvoked.valueがtrueであることをテストしています。

TCAを0.54.0以上にアップデートしていない場合、もしくはTree base, Stack baseのNavigationに置き換えていない場合は以下のコードを使いましょう。

struct TestView: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        HogeView()
            .onChange(of: viewStore.isPresented) { value in
                if !value {
                    dismiss()
                }
            }
    }
}

struct TestReducer: ReducerProtocol {
    struct State: Equatable {
        var isPresented = true
    }
    enum Action: Equatable {
        case someAction
    }
    
    func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
        switch action {
        case .someAction:
           state.isPresented = false
           return .none
        }
    }
}

このようにdismissにしたいタイミングでReducerのisPresentedをfalseにして、
View側で値の変更を検知し、isPresentedがfalseのときにEnvironmentのdismissを読んでいます。

余談

これを知ったのはこの公式チュートリアルなのですが、このようにfireAndForgetが使われています。
スクリーンショット 2023-05-31 17.37.42.png
TCAのバージョン0.53.0からtaskとfireAndForgetがSoft Deprecatedになったはずなのですが、なんでrunを使わなかったのですかね。

単純に理由が気になったのでPull Requestを提出しました。

(追記)
だしたら、「古い習性はなかなか治らない」 と言われ、ただの間違いだったようです
スクリーンショット 2023-06-01 2.34.00.png

参考

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1