5

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:

davidev
  • 7,694
  • 5
  • 21
  • 56
  • Some sideline comments... when you include boilerplate code into a question, there is no need to include the warnings that Apple include... also `.shouldMigrateStoreAutomatically` and `.shouldInferMappingModelAutomatically` both default to `true` so these calls are necessary... finally, there is no need for your call to `UIApplication.shared.registerForRemoteNotifications()` as you should be checking this in your "Signings and Capabilities" tab under "Background Modes". – andrewbuilder Sep 06 '20 at 14:28
  • Core Data entities conform to ObservableObject and are by default published, so the notifications seem entirely unnecessary to me. Maybe I misunderstand the need in your case? – andrewbuilder Sep 06 '20 at 14:48

2 Answers2

3

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...

Project signing and capabilities

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
}()
andrewbuilder
  • 3,629
  • 2
  • 24
  • 46
  • Thank you for the very detailed answer. I have edit everything like you said. You are right, I checked the default settings of the container like you said. However, it is still not working. I always have to restart the application and then it fetch the updates. There is no automatically fetch on macOS or iOS. I have configured remote notifications and set up everything like you said. Can you maybe share your entitlements file? – davidev Sep 07 '20 at 11:31
  • 1
    The entitlements file won't tell you much, they simply hold the value for `iCloud Container Identifiers` and `iCloud Services`. I'm currently working on a universal project written entirely in SwiftUI that targets iOS and macOS, uses Core Data and CloudKit. I'm using `NSPersistentCloudKitContainer` - this works perfectly to sync data between my Mac, iPad Pro and simulators. So I humbly suggest that you have not set up your project correctly. There are so many factors involved that without more information, it is difficult for me to provide advice. – andrewbuilder Sep 07 '20 at 11:43
  • I can recommend two reasonably good tutorials that should help you check your project setup... to prepare the app for iOS follow this first tutorial https://www.alfianlosari.com/posts/building-expense-tracker-ios-app-with-core-data-and-swiftui/, then to add a macOS target follow this second tutorial https://www.alfianlosari.com/posts/building-expense-tracker-ios-macos-app-with-coredata-cloudkit-syncing/. This second tutorial focusses on the project settings necessary to establish a functioning `NSPersistentCloudKitContainer`. – andrewbuilder Sep 07 '20 at 11:45
  • Appreciate your help sir. I will check them in detail and follow it again. I will post you a gif above, which shows my problem. The syncing works perfectly, however not being toggled by default. – davidev Sep 07 '20 at 11:55
  • I have added the gif above. Do you have com.apple.developer.icloud-container-environment in your entitlements? And has APS Environment any influence? – davidev Sep 07 '20 at 12:20
  • Honestly the gif doesn't help me understand your problem. In setting up a CloudKit enabled Core Data app, you should have no need to add to or edit the entitlements file. – andrewbuilder Sep 07 '20 at 12:25
  • The gif should indicate that there are changes made by CloudKit in the background, however they are not being fetched automatically eventhough these changes exist. When I skip out of the app and go back in, it fetches the updates and then you can see removing entities of the list. But why does this not happen automatically. – davidev Sep 07 '20 at 12:28
  • Thanks again for your help. I accept your answer. It is now working for me. I followed your tipps and your links aswell, and I am not quiet sure what fixed it, but I checked all settings again. Thank you for your time – davidev Sep 09 '20 at 12:08
  • 1
    @davidev I had encountered the same issue as you described. The Background Notification seems not working and the list is not updated even if they are modified on another device. May I ask what ends up fixing your issue? – Legolas Wang Jan 04 '21 at 22:28
  • 1
    lol. The mysteries solved on my end. It turns out that it is simply taking too long to update that I thought its not updating. – Legolas Wang Jan 04 '21 at 22:41
2

For anyone looking at this lately, the fix for me after a LOT of searching was simply adding container.viewContext.automaticallyMergesChangesFromParent = true inside the init(inMemory) method in Persistence.swift (all stock from Apple for a SwiftUI in Xcode 12.5.1. As soon as I added that and rebuilt the 2 simulators, everything sync'd w/in 5-15 seconds as it should.

init(inMemory: Bool = false) { 
    container = NSPersistentCloudKitContainer(name: "StoreName") 
    container.viewContext.automaticallyMergesChangesFromParent = true
Ryan Pyeatt
  • 113
  • 6