SwiftUIのViewModelをPropertyWrapperなstructで作成してみる実験

SwiftUIのViewModelには一般的にObservableObjectのクラスが利用されますが、Viewへの組み込みやViewのEnvironment/EnvironmentObjectの参照が行いにくいことが問題になります。

ViewModelから直接Viewの環境のEnvironment/EnvironmentObject使えるようにしたりViewが自動的にViewModelを保持する方法としてViewModel自体をPropertyWrapperとして作成する方法を試してみました。

具体的な実装は以下のようになります。

struct ContentView: View {
    @ContentViewModel var viewModel

    var body: some View {
        Button(viewModel.text) {
            viewModel.changeText("changed")
        }
    }
}

@propertyWrapper
struct ContentViewModel: DynamicProperty {
    @State var text: String = "Hello, world!"

    func changeText(_ value: String) {
        text = value
    }

    var wrappedValue: Self { self }
    init() {}
}

ContentViewModelをPropertyWrapperとして作成し、ContentViewで@ContentViewModelとして保持します。また、DynamicPropertyを利用することでStateやStateObject、ViewのEnvironment/EnvironmentObjectが利用できることを確認しました。

ViewModelが保持する値の更新はDynamicPropertyのupdateメソッドで行うことが可能です。

ViewModelにIDのような初期値を渡す場合には以下のようにprojectedValueを介すことでViewのイニシャライザで値をセットすることができます。

struct ContentView: View {
    init(id: Int) {
        self._viewModel.id = id
    }

    @ContentViewModel var viewModel

    var body: some View {
        Text(String(viewModel.id))
    }
}

@propertyWrapper
struct ContentViewModel: DynamicProperty {
    var id: Int = 0

    var wrappedValue: Self { self }
    var projectedValue: Self { self }
    init() {}
}

id: Intに初期値0を代入していますが、Int!でよければ初期値は不要です。

簡単な実験なので実際に使ってみると予期しない問題が発生する可能性がありますが、興味深い結果になりました。

Ryoichi Izumita

Ryoichi Izumita