TCA 1.7のリリース内容をざっくり理解する

# 目的

TCA 1.7 がリリースされ、Migration Guideも発表されましたが、`@ObservationState`に引っ張られて & そのトピックの多さに正直狼狽えました。
ただ、内容を細かく紐解いていくと自分なりに理解が深まってきたので、Migration Guide に沿って、必要に応じて少し順番を変えつつまとめていきます。
https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7

# `@ObservableState`

新しく導入された`@ObservableState`を利用することで、stateの変更検知に利用していた`ViewStore`を使う必要がなくなりました。

## SwiftUI(iOS17以降を対象とするアプリ)の場合

```swift
 @Reducer
 struct Feature {
   @ObservableState
   struct State { /* ... */ }
   enum Action { /* ... */ }
   var body: some ReducerOf<Self> {
     // ...
   }
 }
 
 struct FeatureView: View {
   let store: StoreOf<Feature>
 
   var body: some View {
      Form {
         Text(store.count.description)
         Button("+") { store.send(.incrementButtonTapped) }
      }
   }
 }
```

## SwiftUI(iOS17未満を対象とするアプリ)の場合
iOS17以降のアプリに対して、こちらはView全体を`WithPerceptionTracking`で囲みます。

```swift
 @Reducer
 struct Feature {
   @ObservableState
   struct State { /* ... */ }
   enum Action { /* ... */ }
   var body: some ReducerOf<Self> {
     // ...
   }
 }
 
 struct FeatureView: View {
   let store: StoreOf<Feature>
 
   var body: some View {
     WithPerceptionTracking {
       Form {
         Text(store.count.description)
         Button("+") { store.send(.incrementButtonTapped) }
       }
     }
   }
 }
```

iOS17以降 or 未満でこれが異なるのは、`WithPerceptionTracking`の中で「Observation Frameworkが利用できるかどうか」を判別しているためです。（iOS17以降なら利用可能）

https://developer.apple.com/documentation/observation

利用できない場合は`swift-perception`というライブラリ(TCAの人が作ってる)で変更の検知を行います。

https://github.com/pointfreeco/swift-perception

## UIKitの場合

```swift
 @Reducer
 struct Feature {
   @ObservableState
   struct State { /* ... */ }
   enum Action { /* ... */ }
   var body: some ReducerOf<Self> {
     // ...
   }
 }
 
 struct FeatureVC: UIViewController {
   let store: StoreOf<Feature>

   @IBOutlet weak var countText: UITextView!

   override func viewDidLoad() {
      super.viewDidLoad()
      observe { [weak self] in
        guard let self else { return }
        countText.text = store.count.description
      }
   }

   @IBAction private func tappedIncrementButton(_ sender: Any) {
      store.send(.incrementButtonTapped)
   }
 }
```

# `IfLetStore` から `if let` へ

※ `State`が`@ObservableState`に準拠している必要があります。

`IfLetStore`は soft-deprecated になりました。
`IfLetStore`は内部で`WithViewStore`を利用して引数`store`の変更検知を行っていましたが、
`State`が`@ObservableState`に準拠している場合は`ViewStore`を利用しなくても変更を検知できるので不要(むしろパフォーマンス悪化の原因になるかも？)です。

こういうのが

```swift
IfLetStore(store: store.scope(state: \.child, action: \.child)) { childStore in
  ChildView(store: childStore)
} else: {
  Text("Nothing to show")
}
```

こう書けるようになります。

```swift
if let childStore = store.scope(state: \.child, action: \.child) {
  ChildView(store: childStore)
} else {
  Text("Nothing to show")
}
```

[ForEachStore](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-ForEachStore-with-ForEach)や[SwitchStore](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-SwitchStore-and-CaseLet-with-switch-and-case)、および[NavigationStackStore](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Replacing-NavigationStackStore-with-NavigationStack)も同様の理由でsoft-deprecatedとなっています。




# `@PresentationState` は `@Presents` に

Swift macrosの制限としてプロパティラッパーと同時に利用できない、というのがあるらしいです。（知らなかった）
これまであった`@PresentationState`はプロパティラッパーで、`@ObservableState`と併用できないので`@Presents`に変わった = プロパティラッパーではなくマクロになりました。

```swift
 @Reducer
 struct Feature {
   @ObservableState
   struct State {
      @Presents var child: Child.State?
   }
   enum Action { /* ... */ }
   var body: some ReducerOf<Self> {
     // ...
   }
 }
```

# `@BindingState`の複雑さの緩和

これまでの歴史的な背景から`@BindingState`関連がかなり複雑化してしまっていた(らしい)のですが、`@ObservableState`の導入によって少し簡単に書けるようになりました。

例えば、`text`と`isOn`をBindable = 双方向の変更が可能な状態にするために、これまでは↓のような書き方をしていたのが
```swift
@Reducer
struct Feature {
  struct State {
    @BindingState var text = ""
    @BindingState var isOn = false
  }
  enum Action: BindableAction {
    case binding(BindingAction<State>)
  }
  var body: some ReducerOf<Self> {
    BindingReducer()
  }
}

struct FeatureView: View {
  let store: StoreOf<Feature>
 
  var body: some View {
    WithViewStore(store, observe: { $0 }) { viewStore in
      Form {
        TextField("Text", text: viewStore.$text)
        Toggle(isOn: viewStore.$isOn)
      }
    }
  }
}
```

`@ObservableState` + `@Bindable`(iOS 16以前をターゲットとする場合は`@Perception.Bindable`)でこのように少しシンプルに書けるようになります。

```swift
@Reducer
struct Feature {
  @ObservableState
  struct State {
    var text = ""
    var isOn = false
  }
  enum Action: BindableAction {
    case binding(BindingAction<State>)
  }
  var body: some ReducerOf<Self> {
    BindingReducer()
  }
}

struct FeatureView: View {
  // iOS 17以降をターゲットとする場合
  @Bindable var store: StoreOf<Feature>
  // iOS 16以前をターゲットとする場合
  // @Perception.Bindable var store: StoreOf<Feature>
 
  var body: some View {
    Form {
      TextField("Text", text: $store.text)
      Toggle(isOn: $store.isOn)
    }
  }
}
```

`viewStore.binding`も同様に書きやすくなっていて、↓のようなbindingが

```swift
Toggle(
  ...,
  isOn: viewStore.binding(get: \.isOn, send: .toggleTapped)
)
```

こんな感じに書けます。

```swift
Toggle(
  ...,
  isOn: $store.isOn.sending(\.toggleTapped)
)
```

# TCAのnavigation modifierからSwiftUIのmodifierへ

これまでは`.sheet`などのnavigation modifierを利用する際、
```swift
.sheet(store: store.scope(state: \.child, action: \.child)) {
   ChildView(store: $0)
}
```
のようにTCAが用意するnavigation modifierを利用していましたが、SwiftUI純正のmodifierを利用できるようになりました。

https://developer.apple.com/documentation/swiftui/view/sheet(item:ondismiss:content:)

```swift
// iOS 17以降をターゲットとする場合
@Bindable var store: StoreOf<Feature>
// iOS 16未満をターゲットとする場合
// @Perception.Bindable var store: StoreOf<Feature>

// ...
.sheet(item: $store.scope(state: \.child, action: \.child)) {
   ChildView(store: $0)
}
```

併せて、これまでの`store`を引数に受け取るTCAのnavigation modifierはdeprecatedとなりました。
`.popover`や`.navigationDestination`も同様にSwiftUIのmodifierが利用可能となり、TCAの`store`を受け取るnavigation modifierがdeprecatedになりました。


また、`.alert()`や`.confirmationDialog()`についても、SwiftUIの`.sheet(item:)`ライクな記述ができるようになりました。
（こちらは以前の書き方がdeprecatedになっているわけではありません）

```swift
// iOS 17以降をターゲットとする場合
@Bindable var store: StoreOf<Feature>
// iOS 16未満もターゲットとする場合
// @Perception.Bindable var store: StoreOf<Feature>

// ...
.alert($store.scope(state: \.alert, action: \.alert))
```

# `ViewStore`が無くなることに付随する更新、代替手法

機能的なアップデートというよりは、`ViewStore`でやっていたことの代替手法などもMigration Guideで紹介されています。

## `ViewState`で算出されていたプロパティ

例えば、`state.firstName`と`state.lastName`を繋げた`fullName`を`ViewState`に持っていた場合、

```swift
let store: StoreOf<Feature>
@ObservedObject private var viewStore: ViewStore<ViewState, Feature.Action>

init(store: StoreOf<Feature>) {
  self.store = store
  self.viewStore = ViewStore(store, observe: ViewState.init(state:))
}

struct ViewState: Equatable {
  let fullName: String
  init(state: Feature.State) {
    self.fullName = "\(state.firstName) \(state.lastName)"
  }
}
```

そのまま`Reducer.State`にComputed Propertyとして持たせることで対応可能です。

```swift
struct State {
  // ...  
  var fullName: String {
    "\(self.firstName) \(self.lastName)"
  }
}
```

## Viewから送信できるアクションを制限する

`Reducer.Action`の分割手法として、下記のようなパターンが提案されており、

```swift
enum Action {
  case view(View)

  enum View {
    case buttonTapped
  }
}
```

これまでの`ViewStore`では、View側からは`Action.View`しか送れないようにすることができました。

```swift
struct FeatureView: View {
    let store: StoreOf<Feature>
    @ObservedObject private var viewStore: ViewStore<ViewState, Feature.Action>
    
    init(store: StoreOf<Feature>) {
      self.store = store
      self.viewStore = ViewStore(
        store,
        observe: ViewState.init(state:),
        // 送れるActionは Feature.Action.view の中身 = enum View のアクションのみ
        send: Feature.Action.view
      )
    }
    
    var body: some View {
      Button(action: { viewStore.send(.buttonTapped) })
    }
}
```

このパターンは1.7でも引き続き利用可能ですが、少しだけ修正を加える必要があります。

まず`Reducer.Action`を`protocol ViewAction`に準拠させます。
https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/viewaction/

```swift
enum Action: ViewAction {
  case view(View)

  enum View {
    case buttonTapped
  }
}
```

そして、View側で`@ViewAction(for:)`マクロを利用することで引き続きこのパターンを利用することができます。

```swift
@ViewAction(for: Feature.self)
struct FeatureView: View {
    let store: StoreOf<Feature>

    var body: some View {
      Button(action: { send(.buttonTapped) })
    }
}
```

# マイグレーションの進め方

「どうやってマイグレーションを徐々に進めれば良いのか」もMigration Guideに書いてくれています。親切...。
https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.7#Incrementally-migrating

細かい話は↑を読んで頂くとして、ポイントは下記の2点です。

- 新しい状態管理方式（`@ObservableState`を使った方法）を利用するFeatureが、従来の`ViewStore`での状態管理を利用するFeatureを内包する場合、その逆よりもパフォーマンスが良い
- つまり、RootのReducerから子孫Reducerへと対応を進めていくとパフォーマンスへの影響が少ない