Using CloudKit + CoreData, how to update UI from remote cloud update without SwiftUI's @FetchRequest?
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).
Solution 1:
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
Thanks to Didier B.
from this thread.