🪂
SwiftUI @StateObjectに引数を使う場合の注意点
問題のコード
期待する動き: 名前を切り替えるたびに、日付の配列が空になって、最初から追加できるようになる。
実際の動き: 名前を切り替えても、日付の配列が空にならず、初期化されない
動作GIF
struct ContentView: View {
@State var selectedName: String = "banana"
var body: some View {
VStack {
let names = ["banana", "lemon", "apple"]
Picker("Picker", selection: $selectedName) {
ForEach(names, id: \.self) { name in
Text(name).tag(name)
}
}
.pickerStyle(.segmented)
let viewModel = SubViewModel(name: selectedName)
SubView(viewModel: viewModel)
}
}
}
class SubViewModel: ObservableObject {
let name: String
@Published var dates: [Date]
init(name: String) {
self.name = name
self.dates = []
}
}
struct SubView: View {
@StateObject var viewModel: SubViewModel
var body: some View {
VStack {
Text(viewModel.name)
Button("Button") {
viewModel.dates.append(.now)
}
ForEach(viewModel.dates, id: \.self) { date in
Text(date.formatted(.dateTime.hour().minute().second()))
}
}
}
}
問題点
SubViewがStateObjectなSubViewModelを持っていて、引数のnameに、ContentViewのselectedNameが使われているので、selectedNameがContentViewで切り替わるたびに、viewModelが新しいものに生成されそうなのですが、実際にはされません。
そのため名前が切り替わっても、datesが初期化されません。
原因
StateObjectは親Viewの変化を監視していないため、引数に変化があっても気づくことができません。
解決方法
Viewに対してid(_:)を付与することで解決できます。
let viewModel = SubViewModel(name: selectedName)
SubView(viewModel: viewModel)
.id(selectedName)
これはAppleのドキュメントに2023年 3月初めぐらいに明記されたようです。
解決方法2(ObservedObejct)
@StateObject
を@ObservedObejct
にすることで問題は解決できるのですが、副作用が大きくなるので、最初の解決方法で済むならそちらの方が良いと思います。
@StateObject var viewModel: SubViewModel
@ObservedObejct var viewModel: SubViewModel
関連記事
Discussion