TCAでsheet表示されたView自身からsheetを閉じる方法


# はじめに

[The Composable Archtecture](https://github.com/pointfreeco/swift-composable-architecture) (TCA) でsheet表示したViewを表示した側から閉じる方法について、とりあえず1パターンだけ示しておきます。

# 結論

- SwiftUIのおさらい
    - SwiftUIではsheetとして表示したViewを閉じる方法としてEnvironmentValuesとして[PresentationMode](https://developer.apple.com/documentation/swiftui/presentationmode)がある
        - `PresentationMode`は`dismiss()`メソッドでViewを閉じれる
            - Viewのみに限定したロジックでViewを閉じたいときはこれだけでいい
- TCAでReducerの処理からViewを閉じたい場合どうするか
    - Stateの変数が変わった場合、SwiftUIの`onChange(of:)`メソッドが検知できる
        - `onChange(of:)`メソッドのクロージャで`PresentationMode`の`dismiss()`を呼び出す


# 詳細

## SwiftUIではsheetとして表示したViewを閉じる方法としてEnvironmentValuesとして[PresentationMode](https://developer.apple.com/documentation/swiftui/presentationmode)がある

サンプルコードとしてContentViewがChildViewをsheetで開き、ChildViewの`PresentationMode`は`dismiss()`メソッドで自身を閉じれる。

![スクリーンショット 2021-04-10 12.05.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16400/de376192-8ee9-9843-a97f-cc85f07b4ebb.png)
)`

```swift
//: A UIKit based Playground for presenting user interface
  
import UIKit
import SwiftUI
import PlaygroundSupport

struct ContentView: View {

    @State var isPresented = false

    var body: some View {
        NavigationView {
            Button(
                action: {
                    print("action:", isPresented) // => false
                    isPresented = true
                },
                label: {
                    Text("Present View")
                }
            )
            .sheet(isPresented: $isPresented) {
                ChildView()
            }
        }
    }
}

struct ChildView: View {
    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        Button(
            action: {
                presentationMode.wrappedValue.dismiss()
            },
            label: {
                Image(systemName: "xmark.square")
                Text("Close")
            }
        )
    }
}

PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
```

Viewのみに限定したロジックでViewを閉じたいときはこれだけでいい。

コメントをもらったので気づけた話として、ContentViewの`isPresented`はChildViewを閉じればfalseになってる。

これは`$isPresented`として`$`をつけることで`@State`から`var projectedValue: Binding<Value> { get }`を取り出せているから。そのために`.sheet(isPresented: $isPresented)`で`seet`からfalseにできる。

https://developer.apple.com/documentation/swiftui/state/projectedvalue


# TCAでReducerの処理からViewを閉じたい場合どうするか

StateのBoolを用意し、`onChange(of:)`でviewStoreを見張ってその変化時に`presentationMode`を操作すればいい。


```swift
struct ChildSttate {
    var isPresented = true
}

struct ChildView: View {
    @Environment(\.presentationMode) var presentationMode

    let store: Store<ChildState, ChildAction>

    var body: some View {
        WithViewStore(store) { viewStore in
            Button(
                action: {
                    presentationMode.wrappedValue.dismiss()
                },
                label: {
                    Image(systemName: "xmark.square")
                    Text("Close")
                }
            )
            .onChange(of: viewStore.isPresented) { value in
                if !value {
                    presentationMode.dismiss()
                }
            }
        }
    }
}
```

# 参考

https://forums.swift.org/t/programatic-dismiss-navigation-animation-based-on-state-binding/39275/7