見出し画像

SwiftUIでViewModelは@ObservedObjectではなく@StateObjectを使うべき

SwiftUIでアプリを開発する時に、アーキテクチャとしてMVVMを採用することがあるが、その際にViewModelは次のようにすれば簡単にデータバインディングできるが、@ObservedObjectではなく@StateObjectにした方がいい。

class ViewModel: ObservableObject {
    @Published var count = 0
    
    func add() {
        count = count + 1
    }
}

struct ContentView: View {
    // ここは@StateObjectにした方がいい
    @ObservedObject var viewModel = ViewModel()
    
    var body: some View {
        Text("Count: \\(viewModel.count)")
        Button {
            viewModel.add()
        } label: {
            Text("add")
        }
    }
}

なぜなら、@ObservedObjectのインスタンスはViewのライフサイクルに依存するので、親Viewが再描画されると、インスタンスが再生成されてしまいデータを保持できなくなるから。

  • @ObservedObject:Viewのライフサイクルに依存し、Viewの再描画でインスタンスは再生成される

  • @StateObject:Viewに対してインスタンスが1つだけ生成される

例えば、次のようにParentViewとChildViewの両方でカウンターがあり、ChildViewの方はChildViewModelがそのカウントを保持・更新できるとする。

ChildViewでカウントアップしてviewModel.add()を実行すると”Child count”は+1されていくが、ParentViewでカウントアップすると、ParentViewのbodyが再描画され、ChildViewも再描画されるためviewModelは初期化されてしまい、”Child count”は0に戻ってしまう。

struct ParentView: View {
    @State var count = 0
    
    var body: some View {
        VStack {
            Text("Parent count: \\(count)")
            Button {
                count += 1
            } label: {
                Text("add")
            }

            ChildView()
        }.frame(width: 400, height: 400)
    }
}

struct ChildView: View {
    @ObservedObject var viewModel = ChildViewModel()
//    @StateObject var viewModel = ChildViewModel()
    
    var body: some View {
        Text("Child count: \\(viewModel.count)")
        Button {
            viewModel.add()
        } label: {
            Text("add")
        }
    }
}

class ChildViewModel: ObservableObject {
    @Published var count = 0
    
    func add() {
        count = count + 1
    }
}

このように、View内でViewModelを保持する場合は@StateObjectを使用すべきである。一方で、親Viewから渡されるデータを参照するのみの場合は、再描画による初期化の影響を受けないので@ObservedObjectを使えばよい。

参考


この記事が気に入ったらサポートをしてみませんか?