💭
[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