CloudKit subscriptions in the private database - ios

I have an app for which I want to add the possibility to backup data to iCloud using CloudKit.
The "backup" part seems to work correctly (my records are in the private database, since they are ... well, private).
Now I would like to use CKSubscriptions to keep all my devices in sync with the same data.
I tried to implement a CKSubscription to monitor record creation / update / deletion based on a query (not based on zones).
func subscribe() {
let options = CKSubscriptionOptions.FiresOnRecordCreation |
CKSubscriptionOptions.FiresOnRecordDeletion |
CKSubscriptionOptions.FiresOnRecordUpdate
let predicate = NSPredicate(value: true) // get all the records for a given type
let subscription = CKSubscription(recordType: "Stocks",
predicate: predicate, subscriptionID: subscriptionID,
options: options)
subscription.notificationInfo = CKNotificationInfo()
subscription.notificationInfo.alertBody = ""
db.saveSubscription(subscription, completionHandler: {
subscription, error in
if (error != nil) {
println("error subscribing: \(error)")
} else {
println("subscribed!")
}
})
}
Until now, I haven't been able to trigger a notification to my device.
I know that you can create a subscription based on zones. Zones are in the private DB, so I suppose that CKSubscriptions could work in the private DB.
But I didn't want to implement zones (that I don't need otherwise).
Question: is there an issue with CKSubscriptions in the private DB based on a query ?

This should work. Non zone subscriptions (query based subscriptions) are supported on private databases. Did you add the code to receive notifications in your AppDelegate?

Related

CloudKit - How to modify the record created by other user

It was OK in development,
but when I distributed my app by TestFlight, I have had this problem.
While I was checking some failures,
I have guessed that when a user who didn’t create the record try to modify it, can’t do that.
By the way, I can fetch all record values on Public Database. Only modification isn’t performed.
In the picture below, iCloud accounts are written in green areas. I predicted that these have to be same.
image: Metadata - CloudKit Dashboard
Now, users are trying to modify a record by the following code:
func modifyRecord() {
let publicDatabase = CKContainer.default().publicCloudDatabase
let predicate = NSPredicate(format: "accountID == %#", argumentArray: [myID!])
let query = CKQuery(recordType: "Accounts", predicate: predicate)
publicDatabase.perform(query, inZoneWith: nil, completionHandler: {(records, error) in
if let error = error {
print("error1: \(error)")
return
}
for record in records! {
/* ↓ New Value */
record["currentLocation"] = CLLocation(latitude: 40.689283, longitude: -74.044368)
publicDatabase.save(record, completionHandler: {(record, error) in
if let error = error {
print("error2: \(error)")
return
}
print("success!")
})
}
})
}
In development, I created and modified all records by myself, so I was not able to find this problem.
Versions
Xcode 11.6 / Swift 5
Summary
I guessed that it is necessary to create and modify record by same user ( = same iCloud Account ) in this code.
Then, could you please tell me how to modify the record created by other user?
In the first place, can I do that?
Thanks.
Looks like you have a permissions problem. In your cloudkit dashboard, go to: Schema > Security Roles
Then under 'Authenticated' (which means a user logged into Icloud)
you'll need to grant 'Write' permissions to the relevant record type. That should fix it!

Mark Notifications as Read in iOS 11 with CloudKit

I have an app that uses CloudKit and everything worked perfectly until iOS 11.
In previous iOS versions, I used a CKQuerySubscription with NSPredicate to receive notifications whenever a user changes a specific table, matching the NSPredicate properties.
After doing so, whenever the server sent notifications, I would iterate through them, fetching its changes and afterwards marking them as READ so I would parse through them again (saving a serverToken).
Now in iOS 11 , Xcode informs me that those delegates are deprecated, and I should change them, but this is where I'm having trouble with - I cannot figure out how to do it in the non-deprecated way, for iOS 11.
Here's my code:
Saving a subscription
fileprivate func setupCloudkitSubscription() {
let userDefaults = UserDefaults.standard
let predicate = NSPredicate(format: /*...*/) // predicate here
let subscription = CKQuerySubscription(recordType: "recordType", predicate: predicate, subscriptionID: "tablename-changes", options: [.firesOnRecordUpdate, .firesOnRecordCreation])
let notificationInfo = CKNotificationInfo()
notificationInfo.shouldSendContentAvailable = true // if true, then it will push as a silent notification
subscription.notificationInfo = notificationInfo
let publicDB = CKContainer.default().publicCloudDatabase
publicDB.save(subscription) { (subscription, err) in
if err != nil {
print("Failed to save subscription:", err ?? "")
return
}
}
}
Check for pending notifications
fileprivate func checkForPendingNotifications() {
let serverToken = UserDefaults.standard.pushNotificationsChangeToken
let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: serverToken)
var notificationIDsToMarkRead = [CKNotificationID]()
operation.notificationChangedBlock = { (notification) -> Void in
if let notificationID = notification.notificationID {
notificationIDsToMarkRead.append(notificationID)
}
}
operation.fetchNotificationChangesCompletionBlock = {(token, err) -> Void in
if err != nil {
print("Error occured fetchNotificationChangesCompletionBlock:", err ?? "")
print("deleting existing token and refetch pending notifications")
UserDefaults.standard.pushNotificationsChangeToken = nil
return
}
let markOperation = CKMarkNotificationsReadOperation(notificationIDsToMarkRead: notificationIDsToMarkRead)
markOperation.markNotificationsReadCompletionBlock = { (notificationIDsMarkedRead: [CKNotificationID]?, operationError: Error?) -> Void in
if operationError != nil {
print("ERROR MARKING NOTIFICATIONS:", operationError ?? "")
return
}
}
let operationQueue = OperationQueue()
operationQueue.addOperation(markOperation)
if token != nil {
UserDefaults.standard.pushNotificationsChangeToken = token
}
}
let operationQueue = OperationQueue()
operationQueue.addOperation(operation)
}
As you can see, the code above works perfectly on iOS until 11;
Now Xcode prompts warning on the following lines:
let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: serverToken)
Warning:
'CKFetchNotificationChangesOperation' was deprecated in iOS 11.0: Instead of iterating notifications to enumerate changed record zones, use CKDatabaseSubscription, CKFetchDatabaseChangesOperation, and CKFetchRecordZoneChangesOperation
AND
let markOperation = CKMarkNotificationsReadOperation(notificationIDsToMarkRead: notificationIDsToMarkRead)
Warning:
'CKMarkNotificationsReadOperation' was deprecated in iOS 11.0: Instead of iterating notifications, consider using CKDatabaseSubscription, CKFetchDatabaseChangesOperation, and CKFetchRecordZoneChangesOperation as appropriate
I tried applying CKDatabaseSubscription but by doing so I cannot apply a NSPredicate to filter the subscription as I can do with CKQuerySubscription, and if I try to fetch and mark pending notifications
as Read it shows those warnings.
What's the best approach for iOS 11 in this case? Any hint?
Thank you.
So as I've mentioned in the comment above in the openradar bug report , which can be found here this is a known issue in iOS 11 when fetching changes for public records and then mark those changes as read, saving the given token.
As there's not a true solution for this issue, because Apple hasn't gave a workaround for this, or maybe not marking those delegate-functions as deprecated until a solution is given I had to go through a different path, which was the follwowing:
I had to create a custom CKRecordZone in the Private Database, then I had to subscribe database changes in that zoneID, by doing so, whenever the user changes something in that database the desired push-notifications and/or silent-push-notifications fire as expected and then I can parse the new data.
My issue here is that I had the User Profile in a public database, so whenever the user changed something related to him (Name, Bio, etc) it saved in CloudKit and silent notifications would fire to the user's other devices to update this data - this I can do perfectly with the private database as well - but my problem was that other users could search for app-users to follow-unfollow them and if that data is stored in the private-database it will be out of general users search scope.
In order to overcome this I had to semi-duplicate the User Profile data.
The user fetches and edits its data through the private-database, and on save it also update a semi-related table in the public-database so it is available to search for general-users.
Until Apple allows us to fetch changes from public-database as we used to do in iOS 10 this solution will work for me temporarily.

How to receive CloudKit notifications about changes made on record shared with me?

I have two iCloud accounts (A and B) on two different devices. From one of them (A) I share ckrecord to another one (B) like this:
let controller = UICloudSharingController { controller, preparationCompletionHandler in
let share = CKShare(rootRecord: record)
share[CKShareTitleKey] = "title" as CKRecordValue
share[CKShareTypeKey] = "pl.blueworld.fieldservice" as CKRecordValue
share.publicPermission = .readWrite
let modifyOperation = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: nil)
modifyOperation.savePolicy = .ifServerRecordUnchanged
modifyOperation.perRecordCompletionBlock = { record, error in
print(error?.localizedDescription ?? "")
}
modifyOperation.modifyRecordsCompletionBlock = { records, recordIds, error in
print(share.url)
preparationCompletionHandler(share, CloudAssistant.shared.container, error)
}
CloudAssistant.shared.container.privateCloudDatabase.add(modifyOperation)
}
controller.delegate = self
UIViewController.top()?.present(controller, animated: true)
When second device (B) did accept cloudkit share I fetch record and subscribe for changes:
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) {
let acceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
acceptSharesOperation.perShareCompletionBlock = { metadata, share, error in
if let error = error {
UIAlertController.show(withMessage: error.localizedDescription)
} else {
let operation = CKFetchRecordsOperation(recordIDs: [cloudKitShareMetadata.rootRecordID])
operation.perRecordCompletionBlock = { record, _, error in
if let error = error {
UIAlertController.show(withMessage: error.localizedDescription)
} else if let record = record {
CloudAssistant.shared.save(records: [record], recordIDsToDelete: [])
let options: CKQuerySubscriptionOptions = [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
let territorySubscription = CKQuerySubscription(recordType: "Territory", predicate: NSPredicate(value: true), options: options)
let notificationInfo = CKNotificationInfo()
notificationInfo.shouldBadge = false
notificationInfo.shouldSendContentAvailable = true
territorySubscription.notificationInfo = notificationInfo
CloudAssistant.shared.sharedDatabase?.save(territorySubscription) { _, _ in }
}
}
CloudAssistant.shared.container.sharedCloudDatabase.add(operation)
}
}
acceptSharesOperation.qualityOfService = .userInteractive
CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptSharesOperation)
}
Now from device A I successfully (I am sure about that, changes is saved in iCloud) perform an update on a record shared with others. But device B doesn't know about that, unless I fetch record manually once again.
But from the other side, it works pretty well.
If I successfully perform an update on a record shared with me (on device B) then device A magically gets a notification about change and everything is fine. What makes the difference?
How to subscribe for changes on a records shared with me?
iOS 11, Swift 4, Xcode 9.
Here's my checklist for debugging subscription notifications not appearing as expected. Sounds like you may have ruled some of these out already.
Make sure the app is registered for notifications
Make sure notifications are enabled on the device for this app
Make sure all devices are using the same container
On the next app startup, read all subscriptions using fetchAllSubscriptionsWithCompletionHandler and NSLog each sub's details (especially: subscriptionID, trigger options, record type and the predicate). Verify that the expected subs exist. Verify that each sub's predicate matches expectations (in this case, compare the predicates you find on both devices).
I wasted a bunch of time debugging "missing" notifications when:
My locally-built version was using the TEST environment and the TestFlight users were accessing the PROD environment. Debugging step 2 found this.
When inadvertently re-using a subscription ID, thus each new sub overwrote the prior one. Debugging step 4 eventually revealed the problem
So far, these four debugging steps have helped me understand all of my "missing notification" problems. Once I understood why the notifs didn't appear, that narrowed down which block of code was responsible.

How do I implement saving 1000s of records using CloudKit and Swift without exceeding 30 or 40 requests/second

Let me describe the basic flow that I am trying to implement:
User logs in
System retrieves list of user's connections using HTTP request to 3rd party API (could be in the 1000s). I'll call this list userConnections
System retrieves stored connections from my app's database (could be in the 100,000s). I'll call this list connections
System then checks to see if each userConnection exists in the connections list already and if not, saves it to database:
for userConnection in userConnections {
if connections.contains(userConnection) {
//do nothing
} else {
saveRecord(userConnection)
}
}
The problem with this is that when the first users log in, the app will try to make 1000 saveRecord calls in a second which the CloudKit server will not allow.
How can I implement this in a different way using CloudKit and Swift so that I keep it to an acceptable number of requests/second, like ~30 or 40?
For anyone wondering, this is how I ended up doing it. The comment by TroyT was correct that you can batch save your records. This answer includes bonus of queued batches:
let save1 = CKModifyRecordsOperation(recordsToSave: list1, recordIDsToDelete: nil)
let save2 = CKModifyRecordsOperation(recordsToSave: list2, recordIDsToDelete: nil)
save1.database = publicDB
save2.database = publicDB
save2.addDependency(save1)
let queue = NSOperationQueue()
queue.addOperations([save1, save2], waitUntilFinished: false)
save1.modifyRecordsCompletionBlock = { savedRecords, deletedRecordsIDs, error in
if (error != nil){
//handle error
}else{
//data saved
}
}

Error using CKSubscription by Zone

I have been experimenting with CloudKit, and so far it's working fine. However, I've been having a hard time getting zone based subscription to work. Has anybody worked on it?. Here the code I tried :
// MARK: - Subscriptions
func registerSubscriptionByZone(){
let container = CKContainer(identifier: "iCloud.com.MyDataInCloudKit")
let database = container.publicCloudDatabase
let recordZone = CKRecordZone.defaultRecordZone()
let zoneSubscription = CKSubscription(zoneID: recordZone.zoneID, options:CKSubscriptionOptions.allZeros)
zoneSubscription.notificationInfo = CKNotificationInfo()
zoneSubscription.notificationInfo.alertBody = "Change in Zone"
self.database.saveSubscription(zoneSubscription) {
subscription, error in
if error != nil {
println(error.localizedDescription)
} else {
println("subscription saved!")
}
}
}
I get the error: Error saving record subscription with id EB6C9478-0A38-4C01-A473-6C09A57B2E8B to server: no valid zone specified
From the CloudKit design guide at:
https://developer.apple.com/library/ios/documentation/General/Conceptual/iCloudDesignGuide/DesigningforCloudKit/DesigningforCloudKit.html
Zones are a useful way to arrange a discrete group of records but are
supported only in private databases. Zones cannot created in a public
database.

Resources