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).
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.
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()
}
}
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
make String
model String

Thanks to Didier B. from this thread.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With