StateObjectのwrappedValueはイニシャライザのclosure内で初期化した方が良い話

# StateObjectのwrappedValueはclosure内で初期化した方が良い話
SwiftUIで、外部から受け取った値を使ってStateObjectを初期化したい場合、イニシャライザのclosure内で初期化しないと無駄にリソースを使ってしまう可能性がありそうです
ObservedObjectとの違いを確認しながら、StateObjectとの挙動を色々確認した備忘録です

https://developer.apple.com/documentation/swiftui/stateobject

```swift
struct HogeView: View {
    @StateObject var vm: HogeViewModel
    init(count: Int) {
        // StateObjectのイニシャライザのclosure内で初期化する例
        _vm = StateObject(wrappedValue: HogeViewModel(count: count))
    }
    // ......
}
```

```swift
struct HogeView: View {
    @StateObject var vm: HogeViewModel
    init(count: Int) {
        // StateObjectのイニシャライザのclosure外で初期化する例
        let vm = HogeViewModel(count: count)
        _vm = StateObject(wrappedValue: vm)
    }
    // ......
}
```

## (参考)StateObject.initの定義
`StateObject.init`のwrappedValueは`@autoclosure`になっており、値を渡しているように見えていますが、closureを渡しています
なので、StateObjectのインスタンス作成と、wrappedValueの作成とはタイミングを変える事が可能になっています

```swift
@frozen @propertyWrapper public struct StateObject<ObjectType> : DynamicProperty where ObjectType : ObservableObject {
    // ......
    @inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)
    // ......
}
```

# ObservedObjectとStateObjectの違い
StateObjectを使うメリットを確認するために、ObservedObjectとStateObjectで挙動の違いを確認しました
ViewとViewModelのイニシャライザでprintしています

```swift
struct HogeView: View { // ObservedObjectを使ったHogeView    
    @ObservedObject var vm: HogeViewModel
    init(count: Int) {
        print("HogeView.init")
        vm = HogeViewModel(count: count)
    }
    var body: some View { Text("Count: \(vm.count)") }
}
// ↑ or ↓ で比較
struct HogeView: View { // StateObjectを使ったHogeView    
    @StateObject var vm: HogeViewModel
    init(count: Int) {
        print("HogeView.init")
        _vm = StateObject(wrappedValue: HogeViewModel(count: count))
    }
    var body: some View { 
        Text("Count: \(vm.count)")
    }
}
class HogeViewModel: ObservableObject { // ViewModelは共通
    @Published var count: Int
    init(count: Int) {
        print("HogeViewModel.init: \(count)")
        self.count = count
    }
}
```

上のHogeViewにNavigationLinkで遷移します

```swift
struct ContentView: View {
    @State var count: Int = 0
    var body: some View {
        NavigationStack {
            Button("Add Count: \(count)") {
                count += 1
            }
            NavigationLink("Link") {
                HogeView(count: count)
            }
        }
    }
}
```
## 実行結果
それぞれのHogeViewにNavigationLinkで遷移した結果がこちらです

|  ObservedObject  |  StateObject  |
| :-: | :-: |
| ![ObservedObject.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/14c9cb2c-5f29-34cf-0873-ad7a1a08d600.gif)|![StateObject.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/1ed18ac2-653a-7e53-b1f7-a7fe2a35f2cb.gif) |

Viewの挙動自体は、一見同じですが、ログに違いがあります
両方ともHogeViewは、ContentViewのcountの更新で作成されます
一方でHogeViewModelは、ObservedObjectの場合はContentViewのcountの更新で、StateObjectの場合は遷移時に作成されます
つまり、**ObservedObjectの場合はViewの作成時に、StateObjectの場合はViewの表示時にViewModelが作成されます**
そのため、ViewModelのイニシャライザ内で何かしら処理を入れている場合、ObservedObjectでは親Viewの更新で無駄にその処理が実行されてしまう問題が発生してしまいます
StateObjectの場合、イニシャライザのclosureでViewModelを受け取っているので、遅延して必要な時だけ作成するという事ができるようです

# StateObjectのwrappedValueはclosure内で初期化した方が良い話
ObservedObjectとStateObjectの違いが見えてきたので、最初の話に戻ります
closure外で初期化した場合、StateObjectがclosure内で必要な時だけ作成してくれる恩恵を受けられなくなります

```swift
struct HogeView: View {
    @StateObject var vm: HogeViewModel
    init(count: Int) {
        print("HogeView.init")
        // closure外で初期化する例
        let vm = HogeViewModel(count: count)
        _vm = StateObject(wrappedValue: vm)
    }
    // ......
}
```

実行してみると、たしかに親Viewの更新でViewModelが再生成されている事がわかります

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/2512fedb-0b33-6120-57b5-67abd738f3ca.gif" width="400">

もし、Viewの外部からViewModelを入れたい場合は、ViewのイニシャライザでもclosureでViewModelを受け取ると、StateObjectの恩恵を受けられるので良さそうです

```swift
struct ContentView: View {
    var body: some View {
        HogeView(viewModel: HogeViewModel(count: count))
    }
}
struct HogeView: View {
    @StateObject var vm: HogeViewModel
    init(viewModel: @autoclosure @escaping () -> HogeViewModel) {
        _vm = StateObject(wrappedValue: viewModel())
    }
    // ......
}
```

# StateObjectを1つのViewの中のViewで使う場合
NavigationLinkで遷移するのではなく、1つのViewの中のViewでStateObjectを使う場合は工夫が必要です

```swift
struct ContentView: View {
    @State var count: Int = 0
    var body: some View {
        VStack {
            Button("Add Count: \(count)") {
                count += 1
            }
            HogeView(count: count)
        }
    }
}
struct HogeView: View {
    @StateObject var vm: HogeViewModel
    init(count: Int) {
        print("HogeView.init")
        _vm = StateObject(wrappedValue: HogeViewModel(count: count))
    }
    var body: some View { Text("Count: \(vm.count)") }
}
```

実行してみると、HogeViewは再生成されていますが、HogeViewModelは古いインスタンスが使い回されるため、値が更新されません
これにより、ContentViewが更新されたとしても、HogeViewは影響を受けずに別で更新させる事ができるのですが、外部から値を受け取る実装になっている場合、意図しない挙動になる事があります

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/dba96fd4-51b0-0061-2dc6-12e868466853.gif" width="400">

SwiftUIはViewごとにIDが振られており、StateObjectはViewのIDを参照してインスタンスを使い回すかどうか判定しているようです
そのため、[`View.id(:)`](https://developer.apple.com/documentation/swiftui/view/id(_:))を使って明示的に更新してあげると、StateObjectも更新されます

```swift
struct ContentView: View {
    @State var count: Int = 0
    var body: some View {
        VStack {
            Button("Add Count: \(count)") {
                count += 1
            }
            HogeView(count: count)
                .id(count) // IDを明示的に更新する
        }
    }
}
struct HogeView: View {
    @StateObject var vm: HogeViewModel
    init(count: Int) {
        print("HogeView.init")
        _vm = StateObject(wrappedValue: HogeViewModel(count: count))
    }
    var body: some View { Text("Count: \(vm.count)") }
}
```

明示的にIDを指定する方法以外にも、[`ForEach.init(_:id:content:)`](https://developer.apple.com/documentation/swiftui/foreach/init(_:id:content:)-82hm4)など、暗黙的にIDが指定される仕組みを使うことで、StateObjectを更新させる事が可能です

# まとめ
StateObjectのwrappedValueはイニシャライザのclosure内で初期化した方が良い話でした
基本的に、StateObjectを使うことでインスタンスが必要に応じて使い回されるため、リソースが効率化されます
しかし、使い所によっては、意図しない形でインスタンスが使い回される事があるので注意する必要がありそうです

```swift
struct HogeView: View {
    @StateObject var vm: HogeViewModel
    init(count: Int) {
        // closure内で初期化
        _vm = StateObject(wrappedValue: HogeViewModel(count: count))
    }
    // ......
}
```