Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating data in public database does not sync with other devices #19

Open
Cyclic opened this issue May 31, 2020 · 6 comments
Open

Creating data in public database does not sync with other devices #19

Cyclic opened this issue May 31, 2020 · 6 comments

Comments

@Cyclic
Copy link

Cyclic commented May 31, 2020

So far, I appear to be creating records successfully using the example app.
image

The first record was created using the simulator, and the second record created using my own physical device. However, it doesn't appear that the two instances of the app receive notifications for sync and so do not appear to be syncing at all. This is the boilerplate example app, which I've modified the entities to use public rather than private, and removed the organization parent as I've mentioned in a previous post.

Do you have any advice on what might be causing the two devices not to sync? In addition, I had a separate user in a separate geographic location use the same container. He is able to create records in the same public CloudKit container, but again, we do not see updates from each other's app instances.

Thanks in advance!

Device A:
image

Device B:
IMG_AECFC89149B3-1

@Cyclic
Copy link
Author

Cyclic commented May 31, 2020

Just an additional note (issue): deleting the app and reinstalling on either device does not fetch any of the data that was created previously on the same device.

@Cyclic
Copy link
Author

Cyclic commented Jun 1, 2020

Example.zip

Adding the project that I'm using in case there are any questions about what I've modified. Notice entity changes and bundle ID. I'm enabling world access and everyone security roles for now if you can test.

Thanks in advance!

@Cyclic
Copy link
Author

Cyclic commented Jun 1, 2020

I suspect it has something to do with self.tokens.tokensByDatabaseScope[database.databaseScope.rawValue]

Since the app is reinstalled or installed to a new device, there are no change tokens present? Is there the need to handle change token changes?

@deeje
Copy link
Owner

deeje commented Jun 1, 2020

Use the class PublicDatabaseSubscriptions to sync public record types.

Also, as I look thru my own app's code, I see that I'm doing a hard sync the first launch to grab all the existing records. That could move into CloudCore itself, but would need to be generalized. Here's my code…

`
static func pullPublic(entityType: String, predicate: NSPredicate? = nil) {
let moc = persistentContainer!.newBackgroundContext()

    let query = CKQuery(recordType: entityType, predicate: predicate ?? NSPredicate(value: true))
    let queryOp = CKQueryOperation(query: query)
    queryOp.recordFetchedBlock = { record in
        let convertOperation = RecordToCoreDataOperation(parentContext: moc, record: record)
        self.queue.addOperation(convertOperation)
    }
    queryOp.queryCompletionBlock = { cursor, error in
        self.queue.addOperation {
            moc.perform {
                try? moc.save()
            }
        }
    }
    CKContainer.default().publicCloudDatabase.add(queryOp)
}

`

@Cyclic
Copy link
Author

Cyclic commented Jun 2, 2020

@deeje thanks for the quick response. I got it working with the code that you suggested. Now I have one further blocker that I think might be in your court and could just be my misunderstanding of how the app is handling persistent change tokens or history. I ran the app on the simulator and added some data. I then ran the app on my physical device and made sure that both apps had the same data while running. The simulator is still running the app from the first launch:
image

You'll notice that the first 4 rows have five employees. I added these manually while my physical device was also running the app to ensure that the changes were coming through, and they did. Both the simulator and the physical device were matched up with employee counts. I made sure to only add employees in the simulator to ensure that notifications were received successfully on the physical device.

I then killed the app on my physical device and deleted it to test data coming in from a fresh install. After installing the app again, the data on my physical device no longer reflects the changes and I'm not exactly sure where the ∆ arrises.
IMG_3201A6659C78-1

I've encountered this behavior several times now, and I'm wondering if there is anything else I need to set for the app to remain in sync even after a fresh install. I've included my AppDelegate here for your reference.

Thanks again in advance!

//
//  AppDelegate.swift
//  CloudTest2
//
//  Created by Vasily Ulianov on 14.02.17.
//  Copyright © 2017 Vasily Ulianov. All rights reserved.
//

import UIKit
import CoreData
import CloudCore
import Reachability
import CloudKit

let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
    
    let delegateHandler = CloudCoreDelegateHandler()
    var reachability: Reachability?
    
    func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // Register for push notifications about changes
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (status, error) in
            if let _ = error {
            }else {
                DispatchQueue.main.async {
                    application.registerForRemoteNotifications()
                }
            }
        }
        
        // Enable uploading changed local data to CoreData
        CloudCore.delegate = delegateHandler
        CloudCore.enable(persistentContainer: persistentContainer)
        
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(reachabilityChanged(notification:)),
                                               name: .reachabilityChanged,
                                               object: reachability)
        
        reachability = try! Reachability(hostname: "icloud.com")
        try? reachability?.startNotifier()
        
        
        pullPublic(entityType: "Organization")
        pullPublic(entityType: "Employee")
        
        return true
    }
    
    @objc private func reachabilityChanged(notification: Notification) {
        let reachability = notification.object as! Reachability
        
        CloudCore.isOnline = reachability.connection != .unavailable
    }
    
    // Notification from CloudKit about changes in remote database
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        // Check if it CloudKit's and CloudCore notification
        if CloudCore.isCloudCoreNotification(withUserInfo: userInfo) {
            // Fetch changed data from iCloud
            CloudCore.pull(using: userInfo, to: persistentContainer, error: {
                print("fetchAndSave from didReceiveRemoteNotification error: \($0)")
            }, completion: { (fetchResult) in
                completionHandler(fetchResult.uiBackgroundFetchResult)
            })
        }
    }
    
    // MARK: - Default Apple initialization, you can skip that
    
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        return true
    }
    
    // MARK: Core Data stack
    
    lazy var persistentContainer: NSPersistentContainer = {
        /*
         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.
         */
        let container = NSPersistentContainer(name: "Model")
        
        if #available(iOS 11.0, *) {
            let storeDescription = container.persistentStoreDescriptions.first
            storeDescription?.setOption(true as NSNumber, forKey:NSPersistentHistoryTrackingKey)
            if #available(iOS 13.0, *) {
                storeDescription?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
            }
        }
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            guard let error = error as NSError? else { return }
            fatalError("###\(#function): Failed to load persistent stores:\(error)")
        })
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        container.viewContext.undoManager = nil
        container.viewContext.shouldDeleteInaccessibleFaults = true
        container.viewContext.automaticallyMergesChangesFromParent = true
        do {
            try container.viewContext.setQueryGenerationFrom(.current)
        } catch {
            fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
        }
        return container
    }()
    
    func pullPublic(entityType: String, predicate: NSPredicate? = nil) {
        let moc = persistentContainer.newBackgroundContext()
        let queue = OperationQueue()
        let query = CKQuery(recordType: entityType, predicate: predicate ?? NSPredicate(value: true))
        let queryOp = CKQueryOperation(query: query)
        queryOp.recordFetchedBlock = { record in
            let convertOperation = RecordToCoreDataOperation(parentContext: moc, record: record)
            queue.addOperation(convertOperation)
        }
        queryOp.queryCompletionBlock = { cursor, error in
            queue.addOperation {
                moc.perform {
                    try? moc.save()
                }
            }
        }
        CKContainer.default().publicCloudDatabase.add(queryOp)
    }
    
    // MARK: Core Data Saving support
    
    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // 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.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
    
}

@Cyclic
Copy link
Author

Cyclic commented Jun 2, 2020

I should also note that CloudKit dashboard reflects the 5 employees for the first 4 companies, so the data is there, it's just not getting fetched from a fresh app install on the separate device.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants