3

Most CloudKit+CoreData tutorials use SwiftUI, and their implementation includes @FetchRequest which automatically detects changes in the CoreData fetch and refreshes the UI.

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

How would I achieve this without SwiftUI? I want to be able to control how I refresh the UI, in response to detecting the CoreData changing due to an iCloud update.

I have this to set up the NSPersistentCloudKitContainer and register for remote notifications:

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)

However I do not know how to handle .NSPersistentStoreRemoteChange the same way the SwiftUI implementation automatically does it. The method is called very frequently from many different threads (many times on startup alone).

Miles
  • 487
  • 3
  • 12
  • 1
    Did you manage to make any progress with this? @FetchRequest seems to be anti-MVVM but I've hit the same problem you mention. – Magnas May 14 '21 at 13:32
  • @Magnas, yea I ended up using a combination of NSFetchedResultController (not just to populate tableViews/collectionViews, but general data management/auto updating) and Combine to sink objectWillChange updates to update the UI – Miles May 15 '21 at 22:53

1 Answers1

6

Here is a complete working example that updates the UI when something changes in CloudKit using CoreData + CloudKit + MVVM. The code related to the notifications is marked with comments, see CoreDataManager and SwiftUI files. Don't forget to add the proper Capabilities in Xcode, see the image below.

Persistence/Data Manager

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)")
        }
    }
}

View Model

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)
        }
    }
}

Core Data Container

ENTITIES

Car

Attributes

make String
model String

Xcode/CloudKit setup

enter image description here

Thanks to Didier B. from this thread.

Deploy the Core Data Schema to CloudKit Production

Please note that there is one final step that needs to be done to make syncing work in a production app that uses CloudKit. Once you're satisfied with the Core Data models and your app is working as expected in the development environment, you need to initialize the schema in CloudKit by running the following code.

    do {
        try container.initializeCloudKitSchema()
    } catch {
        print(error)
    }

Please note that you only need to run the above code when you make changes to the Data Model Container, in other words, you would need to run it before you release the app to the app store for the first time and after that you will only run it if you add or remove Entities or Attributes. Comment out the code after running it.

Please note that after deploying the schema to CloudKit, you will not be able to delete or rename entities or attributes so, make sure your app is working fine and has all of the features you want before deploying the schema to production.

fs_tigre
  • 10,650
  • 13
  • 73
  • 146