使用 CloudKit + CoreData,如何在没有 SwiftUI 的 @FetchRequest 的情况下从远程云更新中更新 UI?

Posted

技术标签:

【中文标题】使用 CloudKit + CoreData,如何在没有 SwiftUI 的 @FetchRequest 的情况下从远程云更新中更新 UI?【英文标题】:Using CloudKit + CoreData, how to update UI from remote cloud update without SwiftUI's @FetchRequest? 【发布时间】:2021-02-06 10:14:01 【问题描述】:

大部分 CloudKit+CoreData 教程使用 SwiftUI,它们的实现包括 @FetchRequest,它会自动检测 CoreData 获取中的变化并刷新 UI。

https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-fetchrequest-property-wrapper

如果没有 SwiftUI,我将如何实现这一目标?我希望能够控制刷新 UI 的方式,以响应检测到由于 iCloud 更新而发生的 CoreData 更改。

我有这个来设置 NSPersistentCloudKitContainer 并注册远程通知:

let storeDescription = NSPersistentStoreDescription()
    storeDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

    let container = NSPersistentCloudKitContainer(name: "CoreDataDemo")
    container.persistentStoreDescriptions = [storeDescription]
    
    container.loadPersistentStores(completionHandler:  (storeDescription, error) in
        if let error = error as NSError? 
            
            fatalError("Unresolved error \(error), \(error.userInfo)")
        
    )
    
    container.viewContext.automaticallyMergesChangesFromParent = true
    container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    
    NotificationCenter.default.addObserver(self, selector: #selector(didReceiveCloudUpdate), name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)

但是,我不知道如何处理 .NSPersistentStoreRemoteChange,就像 SwiftUI 实现自动执行它一样。该方法被许多不同的线程非常频繁地调用(仅在启动时多次)。

【问题讨论】:

您在这方面取得了进展吗? @FetchRequest 似乎是反 MVVM,但我遇到了你提到的同样的问题。 【参考方案1】:

这是一个完整的工作示例,当 CloudKit 中的某些内容发生变化时,它会使用 CoreData + CloudKit + MVVM 更新 UI。与通知相关的代码用 cmets 标记,请参阅 CoreDataManagerSwiftUI 文件。不要忘记在 Xcode 中添加适当的 Capabilities,请参见下图。

持久性/数据管理器

import CoreData
import SwiftUI

class CoreDataManager
    
    static let instance = CoreDataManager()
    let container: NSPersistentCloudKitContainer
    
    let context: NSManagedObjectContext

    init()
        container = NSPersistentCloudKitContainer(name: "CoreDataContainer")
        
       
        guard let description = container.persistentStoreDescriptions.first else
            fatalError("###\(#function): Failed to retrieve a persistent store description.")
        
        
        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)

        // Generate NOTIFICATIONS on remote changes
        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

        container.loadPersistentStores  (description, error) in
            if let error = error
                print("Error loading Core Data. \(error)")
            
        
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

        context = container.viewContext
    
    
    func save()
        do
            try context.save()
            print("Saved successfully!")
        catch let error
            print("Error saving Core Data. \(error.localizedDescription)")
        
    

查看模型

import CoreData

class CarViewModel: ObservableObject
    let manager = CoreDataManager.instance
    @Published var cars: [Car] = []
    
    init()
        getCars()
    

    func addCar(model:String, make:String?)
        let car = Car(context: manager.context)
        car.make = make
        car.model = model

        save()
        getCars()
    
    
    func getCars()
        let request = NSFetchRequest<Car>(entityName: "Car")
        
        let sort = NSSortDescriptor(keyPath: \Car.model, ascending: true)
        request.sortDescriptors = [sort]

        do
            cars =  try manager.context.fetch(request)
        catch let error
            print("Error fetching cars. \(error.localizedDescription)")
        
    
    
    func deleteCar(car: Car)
        manager.context.delete(car)
        save()
        getCars()
    

    func save()
        self.manager.save()
    

SwiftUI

import SwiftUI
import CoreData

struct ContentView: View 
    @StateObject var carViewModel = CarViewModel()
    
    @State private var makeInput:String = ""
    @State private var modelInput:String = ""
    
    // Capture NOTIFICATION changes
    var didRemoteChange = NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange).receive(on: RunLoop.main)

    @State private var deleteCar: Car?
    
    var body: some View 
        NavigationView 
            VStack
                List 
                    if carViewModel.cars.isEmpty 
                        Text("No cars")
                            .foregroundColor(.gray)
                            .fontWeight(.light)
                    
                    ForEach(carViewModel.cars)  car in
                        HStack
                            Text(car.model ?? "Model")
                            Text(car.make ?? "Make")
                                .foregroundColor(Color(UIColor.systemGray2))
                        
                        .swipeActions
                            Button( role: .destructive)
                                carViewModel.deleteCar(car: car)
                            label:
                                Label("Delete", systemImage: "trash.fill")
                            
                        
                    

                
                // Do something on NOTIFICATION
                .onReceive(self.didRemoteChange) _ in
                    carViewModel.getCars()
                

                Spacer()
                Form 
                    TextField("Make", text:$makeInput)
                    TextField("Model", text:$modelInput)
                
                .frame( height: 200)
                
                Button
                    saveNewCar()
                    makeInput = ""
                    modelInput = ""
                label: 
                    Image(systemName: "car")
                    Text("Add Car")
                
                .padding(.bottom)
            
        
    
    
    func saveNewCar()
        if !modelInput.isEmpty
            carViewModel.addCar(model: modelInput, make: makeInput.isEmpty ? nil : makeInput)
        
    

核心数据容器

实体

Car

属性

制作String 型号String

Xcode/CloudKit 设置

感谢来自this 线程的Didier B.

【讨论】:

以上是关于使用 CloudKit + CoreData,如何在没有 SwiftUI 的 @FetchRequest 的情况下从远程云更新中更新 UI?的主要内容,如果未能解决你的问题,请参考以下文章

App 如何根据需要动态为 CoreData 开启 CloudKit 云同步功能

CoreData+CloudKit iOS13 NSPersistentStoreRemoteChangeNotification

如何更灵活的处理 CloudKit 从云端同步到本地的 CoreData 托管对象

如何更灵活的处理 CloudKit 从云端同步到本地的 CoreData 托管对象

CoreData与CloudKit同步时将图像保存到CoreData?

如何通过 Today Extension (iOS 8) 访问 Core Data/CloudKit