[TCA] ViewStateを使用する場合に@BindingStateを使う方法

TCAでアプリを作る際、View特有の変換をしたい場合や不必要な再描画を防ぎパフォーマンスを上げる目的でViewStateの導入をするときがあると思います。
しかし、ViewStateを使う場合、@BindingStateを使用することは現状のTCAのAPIでは提供されていなく、
以下のような便利なAPIは使えないです。
```Swift
viewStore.binding(\.$text)
viewStore.send(.set(\.$text, "")
```
今回はViewStateでもBindingStateの恩恵を受けられるようにコードを書いていきます。

## 方法1 ViewState用のPropertyWrapperを定義する方法
```Swift
@propertyWrapper
public struct ViewStateBinding<State, Value> {
    let keyPath: WritableKeyPath<State, BindingState<Value>>
    let bindingState: BindingState<Value>

    init(_ state: State, _ keyPath: WritableKeyPath<State, BindingState<Value>>) {
        self.keyPath = keyPath
        self.bindingState = state[keyPath: keyPath]
    }

    public var wrappedValue: Value {
        bindingState.wrappedValue
    }

    public var projectedValue: ViewStateBinding<State, Value> {
        self
    }
}

extension ViewStateBinding: Equatable where Value: Equatable {}
extension ViewStateBinding: Hashable where Value: Hashable {}
```
ViewStateBindingでは、ReducerのStateとそのStateのpropertyへのkeyPathを引数に持ち、`wrappedValue`でBindingStateの`wrappedValue`にアクセスできるようにしています。

そしてViewStoreに以下のようなsubscriptを追加します。
```Swift 
extension ViewStore {
    public subscript<ParentState, Value>(
        dynamicMember keyPath: KeyPath<ViewState, ViewStateBinding<ParentState, Value>>
    ) -> Binding<Value>
    where ViewAction: BindableAction, ViewAction.State == ParentState, Value: Equatable  {
        let parentKeyPath = self.state[keyPath: keyPath].keyPath
        return self.binding(
            get: { $0[keyPath: keyPath].wrappedValue },
            send: { .binding(.set(parentKeyPath, $0)) }
        )
    }
}
```
これはSwift5.1で追加された**KeyPath Member Lookup**という機能を使っています。
この機能を使用することでViewStoreがあたかもViewStateのpropertyを持つかのような挙動を実現できます。
getでは、ViewStateBindingの`wrappedValue`プロパティを使用して値を取得し、sendではTCAのActionを使用して、Stateの状態を変更しています。
これによって、2つのkeypathをリンクさせることができ、Bindingの動作を再現することができました。

### 使用方法
```Swift:HogeReducer.swift
struct HogeReducer: ReducerProtocol {
    struct State: Equatable {
        @BindingState var text = ""
    }

    enum Action: Equatable, BindableAction {
        case binding(BindingAction<State>)
    }

    var body: some ReducerProtocolOf<Self> {
        BindingReducer()
    }
}

```

```Swift:HogeView.swift
struct HogeView: View {
    private let store: StoreOf<HogeReducer>
    
    struct ViewState: Equatable {
        @ViewStateBinding<HogeReducer.State, String> var text: String

        init(state: HogeReducer.State) {
            _text = ViewStateBinding(state, \.$text)
        }
    }

    var body: some View {
        WithViewStore(store, observe: ViewState.init) { viewStore in
            TextField("Hoge", text: viewStore.$text)
        }
    }
}

```
ViewStateの定義がすこし面倒ですが、これでViewStateを使用していてもBindingStateを使用することができました。

## 方法2 ViewStoreにメソッドを追加で定義する
こちらの方法の方が個人的には好きです。

```Swift
public extension ViewStore {
    func binding<ParentState, Value>(
        _ parentKeyPath: WritableKeyPath<ParentState, BindingState<Value>>,
        as keyPath: KeyPath<ViewState, Value>
    ) -> Binding<Value>
        where ViewAction: BindableAction, ViewAction.State == ParentState, Value: Equatable
    {
        binding(
            get: { $0[keyPath: keyPath] },
            send: { .binding(.set(parentKeyPath, $0)) }
        )
    }
}
```
やっていることは方法2のViewStoreのsubscript部分とほぼ同じで、
引数に、`parentKeyPath`でReducerProtocolのStateのpropertyへの書き込み可能なKeyPathを、`keyPath`でViewStateのpropertyへの読み込みのみ可能なKeyPathを持つことで、
bindingの`get`でViewStateの値を取得し, `send`ではParentStateへ値を送ることができ、`Binding<Value>`の動作を再現しています。

### 使用方法
HogeReducerは方法1と同じのを利用しています。
```Swift
struct HogeView: View {
    private let store: StoreOf<HogeReducer>
    
    struct ViewState: Equatable {
        let text: String

        init(state: HogeReducer.State) {
            text = state.text
        }
    }

    var body: some View {
        WithViewStore(store, observe: ViewState.init) { viewStore in
            TextField("Hoge", text: viewStore.binding(\.$text, as: \.text))
        }
    }
}
```
こちらの方法では、方法１のようにViewStateでPropertyWrapperを使用する必要もなく、よりスマートに書けているのではないでしょうか。

## 方法3(追記) 公式のやり方
@kalupas226 さんにコメントで教えていただきました、公式のやり方があるそうです！

```Swift:HogeReducer.swift
struct HogeReducer: ReducerProtocol {
    struct State: Equatable {
        @BindingState var text = ""
        
        var view: HogeView.ViewState {
            get { .init(text: text) }
            set { self.text = newValue.text }
        }
    }

    enum Action: Equatable, BindableAction {
        case binding(BindingAction<State>)

        static func view(_ viewAction: HogeView.ViewAction) -> Self {
            switch viewAction {
            case let .binding(action):
                return .binding(action.pullback(\.view))
            }
        }
    }

    var body: some ReducerProtocolOf<Self> {
        BindingReducer()
    }
}
```
```Swift:HogeView
struct HogeView: View {
    private let store: StoreOf<HogeReducer>

    init(store: StoreOf<HogeReducer>) {
        self.store = store
    }

    struct ViewState: Equatable {
        @BindingState var text: String
    }

    enum ViewAction: Equatable, BindableAction {
        case binding(BindingAction<ViewState>)
    }

    var body: some View {
        WithViewStore(store, observe: \.view, send: HogeReducer.Action.view) { viewStore in
            TextField("Hoge", text: viewStore.binding(\.$text))
        }
    }
}
```
方法2では、`viewStore.binding(\.$text, as: \.textFieldText)`のように、parentKeyPathとviewStateのkeyPathとで必ずしもpropertyの名前が同じになるとは限らず、あまりスマートではなかったのですが、これで公式のAPI(`viewStore.binding(\.$text)`)が使えるようになり、方法２の問題も解決しました！

## まとめ
今回はViewStateを使用する場合にBindingStateを使用する方法を紹介しました。
以前は、ViewStateを使用する際、BindingStateの導入を諦めていてTCAの古いBinding方法を使っていたので、かなり利便性が上がったのではないでしょうか。

## 参考文献
https://pointfreeco.github.io/swift-composable-architecture/0.46.0/documentation/composablearchitecture/performance
https://www.pointfree.co/blog/posts/63-the-composable-architecture-%EF%B8%8F-swiftui-bindings
https://github.com/pointfreeco/swift-composable-architecture/discussions/769