jazzedge
11/29/2017 - 6:08 PM

Swift - CloudKit Subscriptions - 1

We do not want to wait for the next time the user launches the App, and we do not want to be fetching the record each time if it has not changed. Instead, we want to minimize the fetches that we make to CloudKit, and only fire a fetch if the record has actually changed. So, how do we do that? The answer is using CloudKit subscriptions.

See: https://www.invasivecode.com/weblog/advanced-cloudkit-i

01. Now when the App launches, instead of directly performing a fetch to CloudKit, what we are going to do first is to check if we have already subscribed to the record changes. If we are subscribed we do not need to do anything else, as we will be notified by CloudKit via remote push notifications. If we are still not subscribed (for example, the first time the user uses the App), before subscribing, we will need to perform a fetch of the record, to be sure that we have the most updated version of the data. Then, we will check if the user has an iCloud account available. In this case, we need an iCloud account to be available, because we are going to save the subscription on CloudKit, and therefore we need write permissions. If the iCloud account is available, we can create a subscription and save it on the public database. The subscription is setup to be fired anytime the record changes.

Let’s add this new method to our AppDelegate and call it when the App launches, instead of calling the other one:

func subscribeToWebServiceSettingsChanges() {
    let subscribed = NSUserDefaults.standardUserDefaults().boolForKey("subscribedToUpdates")
    if subscribed == false {
        let publicDatabase = CKContainer.defaultContainer().publicCloudDatabase
        let recordID = CKRecordID(recordName: "awesomeWebService")
        publicDatabase.fetchRecordWithID(recordID, completionHandler: { (record: CKRecord?, error: NSError?) -> Void in
            guard error==nil else {
                //Handle error here
                return
            }
            if let localRecord = record, let urlString = localRecord["serviceURL"] as? String, let apiKey = localRecord["serviceAPIKey"] as? String {
                self.updateSettingsWithServiceURL(NSURL(string: urlString)!, serviceApiKey:apiKey )
                CKContainer.defaultContainer().accountStatusWithCompletionHandler({ (accountStatus: CKAccountStatus, error: NSError?) -> Void in
                    guard error == nil else {
                        // Handle error here
                        return
                    }
                    if accountStatus == CKAccountStatus.Available {
                        let predicate = NSPredicate(format: "YOUR PREDICATE HERE")
                        let subscription = CKSubscription(recordType: "WebServiceSettings", predicate: predicate, options: CKSubscriptionOptions.FiresOnRecordUpdate)
                        publicDatabase.saveSubscription(subscription, completionHandler: { (subscription: CKSubscription?, error: NSError?) -> Void in
                            guard error == nil else {
                                // Handle error here
                                return
                            }
                            NSUserDefaults.standardUserDefaults().setBool(true, forKey: "subscribedToUpdates")
                            NSUserDefaults.standardUserDefaults().synchronize()
                        })
                    }
                })
            }
        })
    }
}

02. Now, modify the application: didFinishLaunchingWithOptions: in the following way:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 
    subscribeToWebServiceSettingsChanges</code><code>() 
    return true 
}

03. Every time the record changes in CloudKit, a remote push notification will be sent to the device, so we also have to subscribe to receive push notifications and to process them when they arrive. Add this code to the application: didFinishLaunchingWithOptions: to register for push notifications:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
 
    // Push notification setup
    let notificationSettings = UIUserNotificationSettings(forTypes: UIUserNotificationType.Alert, categories: nil)
    application.registerUserNotificationSettings(notificationSettings)
    application.registerForRemoteNotifications()
 
    subscribeToWebServiceSettingsChanges()
    return true
}

04. And finally implement this method in your AppDelegate to handle them:

func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
    let cloudKitNotification = CKNotification.init(fromRemoteNotificationDictionary: userInfo as! [String : NSObject])
    if cloudKitNotification.notificationType == CKNotificationType.Query {
        let recordID = (cloudKitNotification as! CKQueryNotification).recordID
 
        if recordID?.recordName == "awesomeWebService" {
            updateWebServiceSettings()
        }
    }
}

Ok, let’s run the App. Then, go to the CloudKit dashboard and make changes to the record. You will see the the App receives the notification almost immediately and fetches the changes. If we need to make a critical update to any of the fields, now we can propagate the changes to all our users almost immediately.