![見出し画像](https://assets.st-note.com/production/uploads/images/90240752/rectangle_large_type_2_a469a65c78f716b44f820d84d5d886a6.png?width=1200)
SwiftUIでViewModelをDIしてみる
SwiftUIでMVVMを採用する場合、ViewModelは次のように定義することがよくあるが、プレビューやUIテストでViewModelをモックに差し替えられない問題がある。そこで、ViewModelのDI(Dependency Injection)について考えてみたい。
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
Text(viewModel.text)
.padding()
Button {
viewModel.update()
} label: {
Text("Update")
}
}
}
}
@MainActor
class ViewModel: ObservableObject {
@Published var text = "text"
func update() {
// 更新処理
text = "New text"
}
}
まずViewModelが準拠すべきプロトコルを定義して、ViewModelやMockedViewModelをそれに準拠させる。
// Protocolで@MainActorを付ければ、準拠したクラスでも自動的にMainActorになる
@MainActor
protocol ViewModelProtocol: ObservableObject {
var text: String { get }
func update()
}
// ViewModelをViewModelProtocolに準拠させる
final class ViewModel: ViewModelProtocol {
@Published private (set) var text = "text"
func update() {
// 更新処理
text = "New text"
}
}
// モックもViewModelProtocolに準拠させる
final class MockedViewModel: ViewModelProtocol {
@Published private (set) var text = "Dummy text"
func update() {
// モックでの更新処理
text = "New dummy text"
}
}
そしてViewの初期化時にViewModelを受け取ってプロパティに代入することで依存性を注入する。なお、こちらの記事でも紹介したが、ViewModelはViewの再描画時も状態を保持させるために、@ObservedObjectよりも@StateObjectで宣言する方がいい。
@StateObjectの初期化については、公式ドキュメントにinit(wrappedValue:)は直接呼ばないようにと記載されているが、WWDCのデジタルラウンジでDI時の初期化についての質問があり、Appleとしては許容される使い方とのこと(こちらを参照)。
struct ContentView<ViewModel: ViewModelProtocol>: View {
@StateObject private var viewModel: ViewModel
init(viewModel: ViewModel) {
// 渡されたViewModelでStateObjectを初期化することで依存性を注入
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
VStack {
Text(viewModel.text)
.padding()
Button {
viewModel.update()
} label: {
Text("Update")
}
}
}
}
追記
下記のViewModelの初期化方法では、確かに一度のみinitされ依存性の注入で使えるが、Viewが非表示(画面を閉じる、前画面に戻った場合)になってもViewModelがメモリ上に残り続け、メモリ解放されないことがあるようだ。
init(viewModel: ViewModel) {
_viewModel = StateObject(wrappedValue: viewModel)
}
こちらの記事にあるような2回初期化される挙動は確認できていないが、ViewModel.deinit()が呼ばれない事象は発生した。一方View.init()をなくすだけで、同じように初期化でき、かつViewが非表示になるとViewModel.deinit()が呼ばれることを確認できた。
struct ContentView<ViewModel: ViewModelProtocol>: View {
@StateObject private var viewModel: ViewModel
var body: some View {
...
}
}
参考
StateObject について、このような初期化は許容される使い方とのこと。
— トビ (@tobi462) September 24, 2022
続けてドキュメントを更新する気もあると回答しているが...(はよw https://t.co/SSCaWGiRah pic.twitter.com/c17oy2yP0F
この記事が気に入ったらサポートをしてみませんか?