SwiftUIとSwiftDataでiCloud同期対応のアプリを作っていて、他のデバイスでデータが変更された時にViewModelのデータを自動的に更新したいなと思った。CloudKitのimport通知を監視して実現できたのでメモしておく。
背景・課題
SwiftDataでiCloud同期を有効にすると、他のデバイスでのデータ変更が自動的に同期される。しかしViewModelでUIの状態管理をしている場合、ViewModelのデータ配列は自動更新されない。そのため画面に表示されるデータが古いままになってしまう問題があった。
ちなみにSwiftDataを使う場合、@Query を使う手もあるが、AI時代においてテストが書きづらいやり方を取りたくなかった背景もある。
やり方
NSPersistentCloudKitContainerの通知を監視することで解決できた。
import Combine import CoreData import SwiftData @Observable @MainActor class TodoListViewModel { private let modelContext: ModelContext private(set) var todos: [TodoItem] = [] private var cancellables: Set<AnyCancellable> = [] init(modelContext: ModelContext) { self.modelContext = modelContext loadTodos() setupCloudKitNotificationObserver() } /// CloudKitでデータをimportした時に、todos配列を更新する private func setupCloudKitNotificationObserver() { NotificationCenter.default // 1. CloudKitのイベント変更通知を監視する .publisher(for: NSPersistentCloudKitContainer.eventChangedNotification) // 2. 通知からCloudKitイベントオブジェクトを取り出す .compactMap { notification -> NSPersistentCloudKitContainer.Event? in notification.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey] as? NSPersistentCloudKitContainer.Event } // 3. importイベントかつ成功したもののみを通過させる .filter { event in event.type == .import && event.succeeded } // 4. メインスレッドで処理する(UI更新のため) .receive(on: DispatchQueue.main) // 5. データを再読み込みする .sink { [weak self] _ in self?.loadTodos() } // 6. 購読を自動解除できるように .store(in: &cancellables) } /// SwiftDataからTodoアイテムを読み込んでtodos配列を更新 func loadTodos() { let descriptor = FetchDescriptor<TodoItem>(sortBy: [ SortDescriptor(\.order), SortDescriptor(\.id), ]) todos = (try? modelContext.fetch(descriptor)) ?? [] } }
実装のポイントは以下の通り。
NSPersistentCloudKitContainer.eventChangedNotificationを監視する- 通知からイベントを取り出し、typeが
.importかつsucceededがtrueの場合のみ処理する - メインスレッドで受け取るように
.receive(on: DispatchQueue.main)を使う - import成功時に
loadTodos()を呼んでデータを再読み込みする
ハマったポイント
最初はCloudKitの通知を受け取るタイミングがわからなかった。調べてみるとNSPersistentCloudKitContainer.Eventにはsetup、import、exportの3つのタイプがある。他デバイスからのデータ同期時はimportタイプの通知が来るとわかった。また完了時にsucceededがtrueとなることがわかった。そのため、その情報を使ってフィルタリングした。
まとめ
こんな感じでNSPersistentCloudKitContainerの通知を監視することで、iCloud同期のimport成功時にViewModelのデータを更新できた。SwiftDataでiCloud同期対応アプリを作る時の参考になれば。