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

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

# 実装
```Swift
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. <br>
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 で実行時警告が発せられ、解除できないことを知らせます。さらに、この実行時警告は、テストで実行するとテストの失敗となる。 <br>
もし、@Dependency(\.dismiss) を利用した子機能を単独でテストする場合、テストに合格するためには、依存関係をオーバーライドする必要があります。dismissクロージャの内部で共有されたミュータブルな状態を変異させて、それが本当に呼び出されたことを確認することもできます：

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

```Swift
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に置き換えていない場合は以下のコードを使いましょう。
```Swift
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627061/7365acc4-e83f-e880-3109-49d7a0a32bc2.png)
TCAのバージョン0.53.0からtaskとfireAndForgetがSoft Deprecatedになったはずなのですが、なんでrunを使わなかったのですかね。

https://github.com/pointfreeco/swift-composable-architecture/pull/2099/files

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

https://github.com/pointfreeco/swift-composable-architecture/pull/2153

(追記)
だしたら、「古い習性はなかなか治らない」 と言われ、ただの間違いだったようです
![スクリーンショット 2023-06-01 2.34.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627061/9424c829-cac5-c1c9-2ca3-ec0a1b16cdd8.png)

# 参考
https://pointfreeco.github.io/swift-composable-architecture/main/tutorials/composablearchitecture/02-01-yourfirstpresentation

https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/dismisseffect