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が使われています。
TCAのバージョン0.53.0からtaskとfireAndForgetがSoft Deprecatedになったはずなのですが、なんでrunを使わなかったのですかね。
単純に理由が気になったのでPull Requestを提出しました。
(追記)
だしたら、「古い習性はなかなか治らない」 と言われ、ただの間違いだったようです
参考