🪂

SwiftUI @StateObjectに引数を使う場合の注意点

2023/03/09に公開

問題のコード

期待する動き: 名前を切り替えるたびに、日付の配列が空になって、最初から追加できるようになる。
実際の動き: 名前を切り替えても、日付の配列が空にならず、初期化されない

動作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

関連記事

https://note.com/taatn0te/n/n1fc0518a8ee6

Discussion