💭
[SwiftUI]ObservableObject初期化の罠
ObservableObjectの初期化の際の処理
SwiftUIとObservableObjectはほぼ全ての場合で一緒に使用します。ObservableObjectにロジックを集約することでSwiftUI側のコードが簡潔に保たれます。
本題ですが、みなさんはObservableObjectの初期化時(init時)に何かしらの処理を書いていますでしょうか。私の場合は、Combine等を用いて値を受け取ったりする処理を書くことがあります。
class DetailViewModel: ObservableObject {
@Published var items: [String] = []
private let someManager: SomeManager = .shared //シングルトン
private var cancellations = Set<AnyCancellable>()
init() {
debugPrint("[DetailViewModel]init")
someManager.$data
.receive(on: DispatchQueue.main)
.sink { [weak self] data in
debugPrint("fetch data")
self?.items = data.map({ $0.text })
}.store(in: &cancellations)
}
func fetchData() {
someManager.fetchData()
//->データが取得され、someManager.dataが更新されるとします
}
}
このコードは一見問題ないように見えます。
NavigationLinkと一緒に使った場合
次にNavigationLinkも含めてアプリを構築していく場合を考えます。
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink("Detail", destination: DetailView())
}
}
}
struct DetailView: View {
@ObservedObject var viewModel = DetailViewModel()
var body: some View {
VStack {
Text("DetailView")
Text(viewModel.items.join(","))
Button("fetch data", action: { viewModel.fetchData() })
}
}
}
ContentViewでは遷移先を選択します。DetailViewではデータを取得し表示します。
期待する挙動と実際の挙動
このような構成の場合、DetailViewに遷移したらObservableObjectであるDetailViewModelが初期化することが期待されます。
しかし、実際にはContentViewが表示された段階でDetailViewModelも初期化されてしまいます。何故でしょうか。
以下の記事を参考にしました。
この記事によると、SwiftUIの挙動として、NavigationLinkのdestinationに設定するViewはすぐ初期化されてしまうとのこと。おそらくこれはバグではなく、そのような仕様であるということ
対策
このSwiftUIの挙動から対策を2つ考えました。
-
ObservableObjectの初期化時の処理を挙動に注意して実装する - 該当するViewが表示された段階
onAppearに処理を移動する
1については注意深く実装すれば良いのですが、メンテナンスや共同開発時の共有が難しくなる可能性もあります。
2については、onAppearが複数回呼ばれる可能性も考慮しなくてはなりません。
Discussion