I am developing a macOS and iOS app with SwiftUI. Both are using CoreData and iCloudKit to sync data between both platforms. It is indeed working very well with the same iCloud Container.
I am facing a problem that the iCloud background update is not being triggered when staying in the application. If I make changes on both systems, the changes are being pushed, however not visible on the other device.
I need to reload the app, close the app and open again or lose focus in my Mac app and come back to it. Then my List
is going to be refreshed. I am not sure why it is not working, while staying inside the app without losing focus.
I am read several threads here in Stackoverflow, however they are not working for me. This is my simple View in iOS
struct ContentView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@State private var refreshing = false
private var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
@FetchRequest(entity: Person.entity(), sortDescriptors: []) var persons : FetchedResults<Person>
var body: some View {
NavigationView
{
List()
{
ForEach(self.persons, id:\.self) { person in
Text(person.firstName + (self.refreshing ? "" : ""))
// here is the listener for published context event
.onReceive(self.didSave) { _ in
self.refreshing.toggle()
}
}
}
.navigationBarTitle(Text("Person"))
}
}
}
In this example I am already using a workaround, with Asperi described in a different question. However, that isn't working for me either. The list is not being refreshed.
In the logs I can see that it is not pinging the iCloud for refreshing. Only when I reopen the app. Why is background modes not working? I have activate everything properly and set up my AppDelegate.
lazy var persistentContainer: NSPersistentCloudKitContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
container.persistentStoreDescriptions.forEach { storeDesc in
storeDesc.shouldMigrateStoreAutomatically = true
storeDesc.shouldInferMappingModelAutomatically = true
}
//let container = NSPersistentCloudKitContainer(name: "NAME")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
UIApplication.shared.registerForRemoteNotifications()
return container
}()
Edit:
My iOS app only keeps fetching records from iCloud, when the app is being reopened. See this gif:
So apart from my comments and without more information, I suspect you have not set up your project correctly.
Under Signings and Capabilities, your project should look similar to this...
As mentioned I suspect a lot of the code in your ContentView view is unnecessary. Try removing the notifications and simplifying your view code, for example...
struct ContentView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(entity: Person.entity(),
sortDescriptors: []
) var persons : FetchedResults<Person>
var body: some View {
NavigationView
{
List()
{
ForEach(self.persons) { person in
Text(person.firstName)
}
}
.navigationBarTitle(Text("Person"))
}
}
}
With your project correctly setup, CloudKit should handle the necessary notifications and the @FetchRequest
property wrapper will update your data set.
Also, because each Core Data entity is by default Identifiable
, there is no need to reference id:\.self
in your ForEach
statement, so instead of...
ForEach(self.persons, id:\.self) { person in
you should be able to use...
ForEach(self.persons) { person in
As mentioned in the comments, you have included unnecessary code in your var persistentContainer
. It should work as this...
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "NAME")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
return container
}()