[Swift] View から分離した @State が動作しない場合の対応


# 初めに

SwiftUI で開発していると

- `@State`
- `@Binding`
- `@Published`

のようなバインディング（アノテーション）を使用することは増えてくるでしょう。

その際に上手く動作しないことがあり、何度か調べたりすることがあったので、残しておきます。

# 実装

以下のパターンがあったので、順々に説明していきます。

- Case1： View と状態管理クラスを分離する場合
- Case2： 状態管理クラスが階層構造の場合

また、今回のまとめでは `Swift Playgrounds` でテストしたものを使用しています。

## Case1： View と状態管理クラスを分離する場合

### 【前提】

状態管理を含む SwiftUI のコード例は、だいたい **View 内に状態管理のプロパティを保持**していることが多いです。

![sample_1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/2f1bd9c9-2b2c-edab-70e6-29179ee5cf44.png)

当たり前ですが、更新されたテキストが `TextField` に反映されていることがわかります。

<br><br>

ただ、実際にアプリを作る場合、状態管理の部分は **View とは分離する**でしょう。
![sample_2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/acd1abf4-c0db-998e-3646-af574f36a0be.png)

分離先の StateObject クラスで `@State` がうまく動作しません。

### 【解決策】

`@State` はその画面で保持して使用しないと動作しません。
なので今回の場合は `@State` → `@Published` にすることで動作します。
![sample_3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/84b64dce-5c96-f57d-ebd9-6f51910ffc88.png)

無事に動作することが確認できます。

<details>
<summary>Code</summary>


```StateObject.swift
final class StateObject: ObservableObject {
    @Published var bindingText: String = ""
}
```

```ContentView.swift
struct ContentView: View {
    @ObservedObject var stateObject: StateObject
    
    var body: some View {
        TextField("text", text: $stateObject.bindingText)
            .onAppear {
                stateObject.bindingText = "update text"
            }
    }
}
```

</details>

## Case2： 状態管理クラスが階層構造の場合

### 【前提】

状態を分離したコードで、さらにクラスなどを保持することはあるかと思います。
また、そのクラスから値をバインドすること（階層構造の場合）もあるでしょう。
![sample_4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/2893447c-e756-1693-0225-72b5e22ce556.png)

階層構造先のクラスを `ObservableObject` に対応させ、`@ObservedObject` で保持していますが、うまく動作しません。

### 【解決策】

`ObservableObject` の `objectWillChange` を自前で更新することで解決します。
（詳しくは以下を読むと良いでしょう）

https://qiita.com/yimajo/items/8846207d2b7a17ace809


書き方は２パターンあるので、それぞれ載せておきます。

<br>

#### - パターン① `ObservableObject` を使用する

前提にあるコードと同じく、階層構造先の Binding クラスを `ObservableObject` に準拠させている場合です。

![sample_5](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/27da5479-2e6b-4d2d-7dc9-5a3badd646e4.png)

階層構造先の `objectWillChange` をハンドリングして、呼び出し元の `objectWillChange` にハンドリングすることで、反映することができます。

<details>
<summary>Code</summary>


```Binding.swift
final class Binding: ObservableObject {
    @Published var bindingText: String = ""
}
```

```StateObject.swift
final class StateObject: ObservableObject {
    @ObservedObject var binding: Binding = .init()
    private var anyCancellable: AnyCancellable?
    
    init() {
        anyCancellable = binding
            .objectWillChange
            .sink { [weak self] _ in
                self?.objectWillChange.send()
            }
    } 
}
```

```ContentView.swift
struct ContentView: View {
    @ObservedObject var stateObject: StateObject
    
    var body: some View {
        TextField("text", text: $stateObject.binding.bindingText)
            .onAppear {
                stateObject.binding.bindingText = "update text"
            }
    }
}
```

</details>

#### - パターン② `@Published` を使用する

`@Published` で値をバインドさせます。

![sample_6](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/e6ac0792-bbbe-38dc-8790-ea2080af84b5.png)

階層構造先から `$~~~` で値をハンドリングして、呼び出し元の `objectWillChange` にハンドリングすることで、反映することができます。

<details>
<summary>Code</summary>


```Binding.swift
final class Binding {
    @Published var bindingText: String = ""
}
```

```StateObject.swift
final class StateObject: ObservableObject {
    @Published var binding: Binding = .init()
    private var anyCancellable: AnyCancellable?
    
    init() {
        anyCancellable = binding
            .$bindingText
            .sink { [weak self] _ in
                self?.objectWillChange.send()
            }
    } 
}
```

```ContentView.swift
struct ContentView: View {
    @ObservedObject var stateObject: StateObject
    
    var body: some View {
        TextField("text", text: $stateObject.binding.bindingText)
            .onAppear {
                stateObject.binding.bindingText = "update text"
            }
    }
}
```

</details>

### その他

開発の都度、つなぎ目のバインド(`objectWillChange`)を書くのは大変です。
こちらのブログにあるように、プロトコルを作り継承させ、隠蔽化する方法もあります。

https://tech.toreta.in/entry/2019/12/24/104612



以下、該当箇所を抜粋。

```.swift
extension ViewModelObject where Binding.ObjectWillChangePublisher == ObservableObjectPublisher, Output.ObjectWillChangePublisher == ObservableObjectPublisher {

    var objectWillChange: AnyPublisher<Void, Never> {
        return Publishers.Merge(binding.objectWillChange, output.objectWillChange).eraseToAnyPublisher()
    }

}
```


# 終わりに

階層構造の場合に上手く動作しなかったのは、最初は原因がわからずかなり手こずりました。

少なからずアーキテクチャーを導入すると、クラスを分離することになるので、今回のようなパターンは出てくるかと思います。

ご指摘などあればお願いいたしますmm