💭

[SwiftUI]ObservableObject初期化の罠

2021/11/11に公開

ObservableObjectの初期化の際の処理

SwiftUIObservableObjectはほぼ全ての場合で一緒に使用します。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も初期化されてしまいます。何故でしょうか。

以下の記事を参考にしました。
https://stackoverflow.com/questions/57594159/swiftui-navigationlink-loads-destination-view-immediately-without-clicking

この記事によると、SwiftUIの挙動として、NavigationLinkdestinationに設定するViewはすぐ初期化されてしまうとのこと。おそらくこれはバグではなく、そのような仕様であるということ

対策

このSwiftUIの挙動から対策を2つ考えました。

  1. ObservableObjectの初期化時の処理を挙動に注意して実装する
  2. 該当するViewが表示された段階onAppearに処理を移動する

1については注意深く実装すれば良いのですが、メンテナンスや共同開発時の共有が難しくなる可能性もあります。
2については、onAppearが複数回呼ばれる可能性も考慮しなくてはなりません。

Discussion