One simple approach is to first query for the existence of the record. If you receive a successful response of null, then write the record. This leaves open a possibility of a race condition if two devices are both doing this simultaneously, but in that case you can always use the earliest of the returned records.A second and equally simple approach would be to write the record in all cases, but only query for the record with the earliest creationDate, permitting the first written record to be used by all devices.
Post
Replies
Boosts
Views
Activity
I use these with success:Getlet tokenData = // stored locallylet token = try NSKeyedUnarchiver.unarchivedObject(ofClass: CKServerChangeToken.self, from: tokenData)Setlet token: CKServerChangeToken? = the token from CK to be stored locallylet tokenData = try NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: false)// store tokenData locally
I have seen CloudKit deliver 1K of data in under 1 second, but I've also seen it take more 10 seconds or more to deliver the same 1K of data. As far as I know it makes no performance guarantees.(I've used CloudKit many times to move small bits of data between devices, but never for time-critical use. It's certainly quick enough—by the time I've made a change on Device A and look over to Device B, the update's often already complete—but depending on your requirements the occasionally higher latency might make it unsuitable.)
I just encountered this myself. Did you find a solution?
How I encountered it: A creates a ToDo list and shares it with B, which works fine. Both A (owner) and B (subscriber) see the list.
A adds ItemOne to the list, which works fine. Both A and B can now see that the list contains a single item, ItemOne.
B adds ItemTwo to the list, which causes the error you've noted.
In my example, ItemTwo.parent is properly set to the shared list. Seems like it should work, and DOES work when owner A tries it, but not when subscriber B tries it.
Still investigating...
More info. A follow-on error reports that "PrivateDB can't be used to access another user's zone" so I'm guessing that subscriber B is attempting to save to owner A's private database instead of subscriber B's shared database.
Still investigating...
There's nothing of interest in the code, but I did confirm that the operation was indeed enqueued to the private database, though, as you say, it should have been enqueued to the shared database. So, my error!
I should add that I'm trying to understand how this works independently of the sharing controller. I appreciate that the sharing controller offers a "Stop Sharing" button for the owner and a "Remove Me" button for the joined participant. I was wondering about the programmatic approach.
Thank you. Are you suggesting that the parent reference for childA is reused for childB, and the mismatch between the pre-Operation2 parent and the post-Operation1 parent reference is what's causing the error? In other words, that the creation of a parent reference updates/changes the referenced parent, perhaps to update its modification date?
You'll find it in CKContainer.containerIdentifier.
When you configure your app to use CloudKit, 3 databases are created for you automatically: public, private, and shared. You don't create them yourself.
You'll need to read the documentation for a full understanding, but a good place to start is https://developer.apple.com/documentation/cloudkit/designing_and_creating_a_cloudkit_database.
This turned out to be my fault. While it's true that I was refreshing the parent record before saving the child record, I was doing so in parallel with a prior save of another child record of the same parent, and the earlier save was completing before the later refresh.
Swift:
recordA.save() // recordA.parent == parent 1
recordB.save() // recordB.parent == parent 1
Here's what happens in those two calls. lines 1 and 4 were executed synchronously, then respectively initiating the async calls on lines 2 and 5, which in turn initiated the async calls on lines 3 and 6.
The problem was that line 5 was completing before line 3, but line 3 was completing before line 6, so line 6 was failing because parent 1 had changed in the meantime (by line 3).
1: Save record A with parent 1
2: refresh parent 1
3: save record A
4: Save record B with parent 1
5: refresh parent 1
6: save record B
I was naive to initiate lines 1 and 3 together and fortunate that they failed quickly, since they might have worked well—until they didn't.
There are two solutions I can think of here:
Batch records A and B together in a single operation.
Chain A > B so that B is initiated only after A completes.
The former solution is by far the preferable one, but the latter solution made more sense in my case, so the revised approach (which now works fine) looks like this:
In Swift, it looks much friendlier:
recordA.save() {
recordB.save() {
}
1: Save record A with parent 1
2: refresh parent 1
3: save record A
4: Save record B with parent 1
5: refresh parent 1
6: save record B
I still don't know what "chain PCS data" means, but in essence it seems to indicate that the records you're attempting to modify have changed and thus cannot be modified. The solution is to get a fresh copy of those records and try again. In my case, it meant get a fresh copy of the parent before saving record B.
The following is an opinion. Informed, but still an opinion.
1a. If your app's core functionality requires iCloud and the user is not signed into iCloud, display a clear message telling the user that they'll need to sign in to iCloud to use your app and include a button to take them to the system page for signing in. Do not simply display an alert, but rather show it in an attractive, polite, and modeless/full-window presentation.
1b. If your app includes a non-essential feature that requires iCloud and the user is not signed into iCloud, display a modeless notification/affordance to indicate that the feature is inactive and will remain so until they sign into iCloud. The notification might take the form of a special icon positioned where that feature is normally accessed. Tapping that icon (or hovering, on a Mac) might pop up a brief explainer. If the feature's importance warrants it, a more prominent notification might be used. Modeless is better than modal.
Sync should be automatic. Users will expect this if they use your app across multiple devices. Making it automatic may also clarify what you choose to sync. User data should certainly sync, but user settings may not. If I set my background color to blue to match my blue iPhone, I might not appreciate that my silver iPad's background color is now blue as well. Resist the temptation to ask the user whether they'd like to sync this or that. Rather, make that determination yourself after carefully considering the value to the user.
Also, if possible give the user a way to initiate a manual sync—it's reassuring—and display an indicator like a green checkmark to indicate that the sync completed successfully or failed. One common way to provide a manual sync is by including a pull-to-refresh on the scrollview containing the user's content.
I've determined that the icons/avatars are indeed available, but through ContactsKit rather than CloudKit. The images shown are all images you have personally associated with the user's Contact entry in your Contacts app.
How to retrieve the images:
let share = mySharedRecord.share
let participants = share.participants
let userIdentities = participants.map {$0.userIdentity}
let emails = userIdentities.compactMap {$0.lookupInfo?.emailAddress}
let store = CNContactStore()
let keysToFetch = [CNContactImageDataAvailableKey, CNContactImageDataKey, CNContactThumbnailImageDataKey] as [CNKeyDescriptor]
emails.forEach { email in
do {
let predicate = CNContact.predicateForContacts(matchingEmailAddress: email)
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
let imageDatas: [Data] = contacts.compactMap {$0.imageData}
let images = imageDatas.map {UIImage(data: $0)}
return images
} catch {
// Handle the error
return nil
}
}
Supply a perRecordSaveBlock and perRecordDeleteBlock within that operation and maintain the cumulative results there, leaving the modifyRecordsResultBlock free to describe the operation as a whole.
Perhaps you didn't set up subscriptions in Production? You might have set them up in Development, then neglected to do in Production as well.