CloudKit, after reinstalling an app how do I reset my subscriptions to current status of records? - ios

I'm dealing with the scenario, where a user has previously deleted the app and has now re-installed it.
It was hitting my delta fetch function, which is receiving a lot of old subscription notifications, mostly deletes. But not downloading current records.
I'm now adding code to perform a fetch on each record type to download all the data.
I'd like to reset delta fetch server token, so the app doesn't have to process old subscriptions notifications. However I can't find how to do this, maybe it's not possible.

Are you referring to CKServerChangeToken (documentation) when you say "delta fetch server token"? And are you attempting to sync within the CloudKit private database?
Assuming that is true, here is an example of how I fetch changes from the private database and keep track of the sync token:
//MARK: Fetch Latest from CloudKit from private DB
func fetchPrivateCloudKitChanges(){
print("Fetching private changes...")
//:::
let privateZoneId = CKRecordZone.ID(zoneName: CloudKit.zoneName, ownerName: CKCurrentUserDefaultName)
/----
let options = CKFetchRecordZoneChangesOperation.ZoneOptions()
options.previousServerChangeToken = previousChangeToken
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: [privateZoneId], optionsByRecordZoneID: [recordZoneID:options])
//Queue up the updated records to process below
var records = [CKRecord]()
operation.recordChangedBlock = { record in
records.append(record)
}
operation.recordWithIDWasDeletedBlock = { recordId, type in
//Process a deleted record in your local database...
}
operation.recordZoneChangeTokensUpdatedBlock = { (zoneId, token, data) in
// Save new zone change token to disk
previousChangeToken = token
}
operation.recordZoneFetchCompletionBlock = { zoneId, token, _, _, error in
if let error = error {
print(error)
}
// Write this new zone change token to disk
previousChangeToken = token
}
operation.fetchRecordZoneChangesCompletionBlock = { error in
if let error = error {
print(error
}else{
//Success! Process all downloaded records from `records` array above...
//records...
}
}
CloudKit.privateDB.add(operation)
}
//Change token property that gets saved and retrieved from UserDefaults
var previousChangeToken: CKServerChangeToken? {
get {
guard let tokenData = defaults.object(forKey: "previousChangeToken") as? Data else { return nil }
return NSKeyedUnarchiver.unarchiveObject(with: tokenData) as? CKServerChangeToken
}
set {
guard let newValue = newValue else {
defaults.removeObject(forKey: "previousChangeToken")
return
}
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
defaults.set(data, forKey: "previousChangeToken")
}
}
Your specific situation might differ a little, but I think this is how it's generally supposed to work when it comes to staying in sync with CloudKit.
Update
You could try storing the previousServerChangeToken on the Users record in CloudKit (you would have to add it as a new field). Each time the previousServerChangeToken changes in recordZoneFetchCompletionBlock you would have to save it back to iCloud on the user's record.

Related

How to fetch all changes since latest CKServerChangeToken?

This is how I define fetching changes:
func fetchAllChanges(isFetchedFirstTime: Bool) {
let zone = CKRecordZone(zoneName: "fieldservice")
let options = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
options.previousServerChangeToken = Token.privateZoneServerChangeToken //initially it is nil
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: [zone.zoneID], configurationsByRecordZoneID: [zone.zoneID: options])
operation.fetchAllChanges = isFetchedFirstTime
operation.database = CloudAssistant.shared.privateDatabase
// another stuff
}
When I fetch all of them first time, then fetchAllChanges is false. So I only get server change token and save it for another use. No changes for records is returned. And it is ok;)
The problem is when I try to fetch it SECOND TIME. Since then nothing changed, server change token is not nil now, but fetchAllChanges is true because I need all the changes since first fetch (last server change token). It should work like this in my opinion.
But the SECOND TIME I got ALL THE CHANGES from my cloudkit (a few thousands of records and alll the changes). Why? I thought I told cloudkit that I do not want it like this. What am I doing wrong?
I have implemented #vadian answer, but my allChanges is always empty. Why?
func fetchPrivateLatestChanges(handler: ProgressHandler?) async throws -> ([CKRecord], [CKRecord.ID]) {
/// `recordZoneChanges` can return multiple consecutive changesets before completing, so
/// we use a loop to process multiple results if needed, indicated by the `moreComing` flag.
var awaitingChanges = true
var changedRecords = [CKRecord]()
var deletedRecordIDs = [CKRecord.ID]()
let zone = CKRecordZone(zoneName: "fieldservice")
while awaitingChanges {
/// Fetch changeset for the last known change token.
print("🏆TOKEN: - \(lastChangeToken)")
let allChanges = try await privateDatabase.recordZoneChanges(inZoneWith: zone.zoneID, since: lastChangeToken)
/// Convert changes to `CKRecord` objects and deleted IDs.
let changes = allChanges.modificationResultsByID.compactMapValues { try? $0.get().record }
print(changes.count)
changes.forEach { _, record in
print(record.recordType)
changedRecords.append(record)
handler?("Fetching \(changedRecords.count) private records.")
}
let deletetions = allChanges.deletions.map { $0.recordID }
deletedRecordIDs.append(contentsOf: deletetions)
/// Save our new change token representing this point in time.
lastChangeToken = allChanges.changeToken
/// If there are more changes coming, we need to repeat this process with the new token.
/// This is indicated by the returned changeset `moreComing` flag.
awaitingChanges = allChanges.moreComing
}
return (changedRecords, deletedRecordIDs)
}
And here is what is repeated on console:
🏆TOKEN: - nil
0
🏆TOKEN: - Optional(<CKServerChangeToken: 0x1752a630; data=AQAAAAAAAACXf/////////+L6xlFzHtNX6UXeP5kslOE>)
0
🏆TOKEN: - Optional(<CKServerChangeToken: 0x176432f0; data=AQAAAAAAAAEtf/////////+L6xlFzHtNX6UXeP5kslOE>)
0
🏆TOKEN: - Optional(<CKServerChangeToken: 0x176dccc0; data=AQAAAAAAAAHDf/////////+L6xlFzHtNX6UXeP5kslOE>)
0
... ...
This is how I use it:
TabView {
//my tabs
}
.tabViewStyle(PageTabViewStyle())
.task {
await loadData()
}
private func loadData() async {
await fetchAllInitialDataIfNeeded { error in
print("FINITO>>🏆")
print(error)
}
}
private func fetchAllInitialDataIfNeeded(completion: #escaping ErrorHandler) async {
isLoading = true
do {
let sthToDo = try await assistant.fetchPrivateLatestChanges { info in
self.loadingText = info
}
print(sthToDo)
} catch let error as NSError {
print(error.localizedDescription)
}
Assuming you have implemented also the callbacks of CKFetchRecordZoneChangesOperation you must save the token received by the callbacks permanently for example in UserDefaults.
A smart way to do that is a computed property
var lastChangeToken: CKServerChangeToken? {
get {
guard let tokenData = UserDefaults.standard.data(forKey: Key.zoneChangeToken) else { return nil }
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: CKServerChangeToken.self, from: tokenData)
}
set {
if let token = newValue {
let tokenData = try! NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
UserDefaults.standard.set(tokenData, forKey: Key.zoneChangeToken)
} else {
UserDefaults.standard.removeObject(forKey: Key.zoneChangeToken)
}
}
}
The struct Key is for constants, you can add more keys like the private subscription ID etc.
struct Key {
let zoneChangeToken = "zoneChangeToken"
}
Secondly I highly recommend to use the async/await API to fetch the latest changes because it get's rid of the complicated and tedious callbacks.
As you have a singleton CloudAssistant implement the method there and use a property constant for the zone. In init initialize the privateDatabase and also the zone properties.
This is the async/await version of fetchLatestChanges, it returns the new records and also the deleted record IDs
/// Using the last known change token, retrieve changes on the zone since the last time we pulled from iCloud.
func fetchLatestChanges() async throws -> ([CKRecord], [CKRecord.ID]) {
/// `recordZoneChanges` can return multiple consecutive changesets before completing, so
/// we use a loop to process multiple results if needed, indicated by the `moreComing` flag.
var awaitingChanges = true
var changedRecords = [CKRecord]()
var deletedRecordIDs = [CKRecord.ID]()
while awaitingChanges {
/// Fetch changeset for the last known change token.
let allChanges = try await privateDatabase.recordZoneChanges(inZoneWith: zone, since: lastChangeToken)
/// Convert changes to `CKRecord` objects and deleted IDs.
let changes = allChanges.modificationResultsByID.compactMapValues { try? $0.get().record }
changes.forEach { _, record in
changedRecords.append(record)
}
let deletetions = allChanges.deletions.map { $0.recordID }
deletedRecordIDs.append(contentsOf: deletetions)
/// Save our new change token representing this point in time.
lastChangeToken = allChanges.changeToken
/// If there are more changes coming, we need to repeat this process with the new token.
/// This is indicated by the returned changeset `moreComing` flag.
awaitingChanges = allChanges.moreComing
}
return (changedRecords, deletedRecordIDs)
}
I believe you misunderstand how this works. The whole point of passing a token to the CKFetchRecordZoneChangesOperation is so that you only get the changes that have occurred since that token was set. If you pass nil then you get changes starting from the beginning of the lifetime of the record zone.
The fetchAllChanges property is very different from the token. This property specifies whether you need to keep calling a new CKFetchRecordZoneChangesOperation to get all of the changes since the given token or whether the framework does it for you.
On a fresh install of the app you would want to pass nil for the token. Leave the fetchAllChanges set to its default of true. When the operation runs you will get every change ever made to the record zone. Use the various completion blocks to handle those changes. In the end you will get an updated token that you need to save.
The second time you run the operation you use the last token you obtained from the previous run of the operation. You still leave fetchAllChanges set to true. You will now get only the changes that may have occurred since the last time you ran the operation.
The documentation for CKFetchRecordZoneChangesOperation shows example code covering all of this.

Listener event not triggered when document is updated (Google Firestore)

I am struggling to understand why my event listener that I initialize on a document is not being triggered whenever I update the document within the app in a different UIViewController. If I update it manually in Google firebase console, the listener event gets triggered successfully. I am 100% updating the correct document too because I see it get updated when I update it in the app. What I am trying to accomplish is have a running listener on the current user that is logged in and all of their fields so i can just use 1 global singleton variable throughout my app and it will always be up to date with their most current fields (name, last name, profile pic, bio, etc.). One thing I noticed is when i use setData instead of updateData, the listener event gets triggered. For some reason it doesn't with updateData. But i don't want to use setData because it will wipe all the other fields as if it is a new doc. Is there something else I should be doing?
Below is the code that initializes the Listener at the very beginning of the app after the user logs in.
static func InitalizeWhistleListener() {
let currentUser = Auth.auth().currentUser?.uid
let userDocRef = Firestore.firestore().collection("users").document(currentUser!)
WhistleListener.shared.listener = userDocRef.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
print("INSIDE LISTENER")
}
}
Below is the code that update's this same document in a different view controller whenever the user updates their profile pic
func uploadProfilePicture(_ image: UIImage) {
guard let uid = currentUser!.UID else { return }
let filePath = "user/\(uid).jpg"
let storageRef = Storage.storage().reference().child(filePath)
guard let imageData = image.jpegData(compressionQuality: 0.75) else { return }
storageRef.putData(imageData) { metadata, error in
if error == nil && metadata != nil {
self.userProfileDoc!.updateData([
"profilePicURL": filePath
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}
}
}
You can use set data with merge true it doesn't wipe any other property only merge to specific one that you declared as like I am only update the name of the user without wiping the age or address
db.collection("User")
.document(id)
.setData(["name":"Zeeshan"],merge: true)
The answer is pretty obvious (and sad at the same time). I was constantly updating the filepath to be the user's UID therefore, it would always be the same and the snapshot wouldn't recognize a difference in the update. It had been some time since I had looked at this code so i forgot this is what it was doing. I was looking past this and simply thinking an update (no matter if it was different from the last or not) would trigger an event. That is not the case! So what I did was append an additional UUID to the user's UID so that it changed.

How to get ObjectID and search for specific ObjectID in CoreData in Swift 5?

I am currently working on a project with a multi user system. The user is able to create new profiles which are saved persistently using CoreData.
My problem is: Only one profile can be the active one at a single time, so I would like to get the ObjectID of the created profile and save it to UserDefaults.
Further I was thinking that as soon as I need the data of the active profile, I can simply get the ObjectID from UserDefaults and execute a READ - Request which only gives me back the result with that specific ObjectID.
My code so far for SAVING THE DATA:
// 1. Create new profile entry to the context.
let newProfile = Profiles(context: context)
newProfile.idProfileImage = idProfileImage
newProfile.timeCreated = Date()
newProfile.gender = gender
newProfile.name = name
newProfile.age = age
newProfile.weight = weight
// 2. Save the Object ID to User Defaults for "activeUser".
// ???????????????????
// ???????????????????
// 3. Try to save the new profile by saving the context to the persistent container.
do {
try context.save()
} catch {
print("Error saving context \(error)")
}
My code so far for READING THE DATA
// 1. Creates an request that is just pulling all the data.
let request: NSFetchRequest<Profiles> = Profiles.fetchRequest()
// 2. Try to fetch the request, can throw an error.
do {
let result = try context.fetch(request)
} catch {
print("Error reading data \(error)")
}
As you can see, I haven't been able to implement Part 2 of the first code block. The new profile gets saved but the ObjectID isn't saved to UserDefaults.
Also Party 1 of the second code block is not the final goal. The request just gives you back all the data of that entity, not only the one with the ObjectID I stored in User Defaults.
I hope you guys have an idea on how to solve this problem.
Thanks for your help in advance guys!
Since NSManagedObjectID does not conform to one of the types handled by UserDefaults, you'll have to use another way to represent the object id. Luckily, NSManagedObjectID has a uriRepresentation() that returns a URL, which can be stored in UserDefaults.
Assuming you are using a NSPersistentContainer, here's an extension that will handle the storage and retrieval of a active user Profile:
extension NSPersistentContainer {
private var managedObjectIDKey: String {
return "ActiveUserObjectID"
}
var activeUser: Profile? {
get {
guard let url = UserDefaults.standard.url(forKey: managedObjectIDKey) else {
return nil
}
guard let managedObjectID = persistentStoreCoordinator.managedObjectID(forURIRepresentation: url) else {
return nil
}
return viewContext.object(with: managedObjectID) as? Profile
}
set {
guard let newValue = newValue else {
UserDefaults.standard.removeObject(forKey: managedObjectIDKey)
return
}
UserDefaults.standard.set(newValue.objectID.uriRepresentation(), forKey: managedObjectIDKey)
}
}
}
This uses a method on NSPersistentStoreCoordinator to construct a NSManagedObjectID from a URI representation.

CloudKit Sharing

I am having trouble understanding some of the CloudKit sharing concepts and the WWDC 2016 "What's new in CloudKit" video doesn't appear to explain everything that is required to allow users to share and access shared records.
I have successfully created an app that allows the user to create and edit a record in their private database.
I have also been able to create a Share record and share this using the provided sharing UIController. This can be successfully received and accepted by the participant user but I can't figure out how to query and display this shared record.
The app creates a "MainZone" in the users private database and then creates a CKRecord in this "MainZone". I then create and save a CKShare record and use this to display the UICloudSharingController.
How do I query the sharedDatabase in order to access this record ? I have tried using the same query as is used in the privateDatabase but get the following error:
"ShareDB can't be used to access local zone"
EDIT
I found the problem - I needed to process the accepted records in the AppDelegate. Now they appear in the CloudKit dashboard but I am still unable to query them. It seems I may need to fetch the sharedDatabase "MainZone" in order to query them.
Dude, I got it: First you need to get the CKRecordZone of that Shared Record. You do it by doing the following:
let sharedData = CKContainer.default().sharedCloudDatabase
sharedData.fetchAllRecordZones { (recordZone, error) in
if error != nil {
print(error?.localizedDescription)
}
if let recordZones = recordZone {
// Here you'll have an array of CKRecordZone that is in your SharedDB!
}
}
Now, with that array in hand, all you have to do is fetch normally:
func showData(id: CKRecordZoneID) {
ctUsers = [CKRecord]()
let sharedData = CKContainer.default().sharedCloudDatabase
let predicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: "Elder", predicate: predicate)
sharedData.perform(query, inZoneWith: id) { results, error in
if let error = error {
DispatchQueue.main.async {
print("Cloud Query Error - Fetch Establishments: \(error)")
}
return
}
if let users = results {
print(results)
self.ctUsers = users
print("\nHow many shares in cloud: \(self.ctUsers.count)\n")
if self.ctUsers.count != 0 {
// Here you'll your Shared CKRecords!
}
else {
print("No shares in SharedDB\n")
}
}
}
}
I didn't understand quite well when you want to get those informations. I'm with the same problem as you, but I only can get the shared data by clicking the URL... To do that you'll need two functions. First one in AppDelegate:
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) {
let acceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
acceptSharesOperation.perShareCompletionBlock = {
metadata, share, error in
if error != nil {
print(error?.localizedDescription)
} else {
let viewController: ViewController = self.window?.rootViewController as! ViewController
viewController.fetchShare(cloudKitShareMetadata)
}
}
CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptSharesOperation)
}
in ViewConroller you have the function that will fetch this MetaData:
func fetchShare(_ metadata: CKShareMetadata) {
let operation = CKFetchRecordsOperation(recordIDs: [metadata.rootRecordID])
operation.perRecordCompletionBlock = { record, _, error in
if error != nil {
print(error?.localizedDescription)
}
if record != nil {
DispatchQueue.main.async() {
self.currentRecord = record
//now you have your Shared Record
}
}
}
operation.fetchRecordsCompletionBlock = { _, error in
if error != nil {
print(error?.localizedDescription)
}
}
CKContainer.default().sharedCloudDatabase.add(operation)
}
As I said before, I'm now trying to fetch the ShareDB without accessing the URL. I don't want to depend on the link once I already accepted the share. Hope this helps you!

Swift CloudKit SaveRecord "Error saving record"

I am trying to save a record to CloudKit but am getting an error. I had seen elsewhere that this was an issue that required knowing how to save but I can't get this to work.
var database:CKDatabase = CKContainer.defaultContainer().publicCloudDatabase
var aRecord:CKRecord!
if self.cloudId == nil {
var recordId:CKRecordID = CKRecordID(recordName: "RecordId")
self.cloudId = recordId // Setup at top
}
aRecord = CKRecord(recordType: "RecordType", recordID: self.cloudId)
aRecord.setObject(self.localId, forKey: "localId")
// Set the normal names etc
aRecord.setObject(self.name, forKey: "name")
var ops:CKModifyRecordsOperation = CKModifyRecordsOperation()
ops.savePolicy = CKRecordSavePolicy.IfServerRecordUnchanged
database.addOperation(ops)
database.saveRecord(aRecord, completionHandler: { (record, error) in
if error != nil {
println("There was an error \(error.description)!")
} else {
var theRecord:CKRecord = record as CKRecord
self.cloudId = theRecord.recordID
}
})
This gives me the error:
There was an error <CKError 0x16d963e0: "Server Record Changed" (14/2017); "Error saving record <CKRecordID: 0x15651730; xxxxxx:(_defaultZone:__defaultOwner__)> to server: (null)"; uuid = 369226C6-3FAF-418D-A346-49071D3DD70A; container ID = "iCloud.com.xxxxx.xxxx-2">!
Not sure, given that I have added CKModifyRecordsOperation. Sadly there is no examples within Apple's documentation. I miss that (which you get on MSDN).
Thanks peeps!
A record can be saved to iCloud using CKDatabase's convenience method saveRecord: or via a CKModifyRecordsOperation. If it's a single record, you can use saveRecord: but will need to fetch the record you'd like to modify using fetchRecordWithID: prior to saving it back to iCloud. Otherwise, it will only let you save a record with a new RecordID. More here.
database.fetchRecordWithID(recordId, completionHandler: { record, error in
if let fetchError = error {
println("An error occurred in \(fetchError)")
} else {
// Modify the record
record.setObject(newName, forKey: "name")
}
}
database.saveRecord(aRecord, completionHandler: { record, error in
if let saveError = error {
println("An error occurred in \(saveError)")
} else {
// Saved record
}
}
The code above is only directionally correct but won't work as is because by the time the completionHandler of fetchRecordWithID returns, saveRecord will have fired already. A simple solution would be to nest saveRecord in the completionHandler of fetchRecordWithID. A probably better solution would be to wrap each call in a NSBlockOperation and add them to an NSOperationQueue with saveOperation dependent on fetchOperation.
This part of your code would be for a CKModifyRecordsOperation and not needed in case you are only updating a single record:
var ops:CKModifyRecordsOperation = CKModifyRecordsOperation()
ops.savePolicy = CKRecordSavePolicy.IfServerRecordUnchanged
database.addOperation(ops)
If you do use a CKModifyRecordsOperation instead, you'll also need to set at least one completion block and deal with errors when conflicts are detected with existing records:
let saveRecordsOperation = CKModifyRecordsOperation()
var ckRecordsArray = [CKRecord]()
// set values to ckRecordsArray
saveRecordsOperation.recordsToSave = ckRecordsArray
saveRecordsOperation.savePolicy = .IfServerRecordUnchanged
saveRecordsOperation.perRecordCompletionBlock { record, error in
// deal with conflicts
// set completionHandler of wrapper operation if it's the case
}
saveRecordsOperation.modifyRecordsCompletionBlock { savedRecords, deletedRecordIDs, error in
// deal with conflicts
// set completionHandler of wrapper operation if it's the case
}
database.addOperation(saveRecordsOperation)
There isn't much sample code yet besides the CloudKitAtlas demo app, which is in Objective-C. Hope this helps.
Generally speaking, you have unitary methods (like saveRecord), which deal with only one record at a time, and mass operations (like CKModifyRecordsOperation), which deal with several records at the same time.
These save operations can be used to save records, or to update records (that is, fetch them, apply changes to them, and then save them again).
SAVE examples:
You create a record and want to save it to CloudKit DB:
let database = CKContainer.defaultContainer().publicCloudDatabase
var record = CKRecord(recordType: "YourRecordType")
database.saveRecord(record, completionHandler: { (savedRecord, saveError in
if saveError != nil {
println("Error saving record: \(saveError.localizedDescription)")
} else {
println("Successfully saved record!")
}
})
You create a bunch of records and you want to save them all at once:
let database = CKContainer.defaultContainer().publicCloudDatabase
// just an example of how you could create an array of CKRecord
// this "map" method in Swift is so useful
var records = anArrayOfObjectsConvertibleToRecords.map { $0.recordFromObject }
var uploadOperation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
uploadOperation.savePolicy = .IfServerRecordUnchanged // default
uploadOperation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordsIDs, error in
if error != nil {
println("Error saving records: \(error.localizedDescription)")
} else {
println("Successfully saved records")
}
}
database.addOperation(uploadOperation)
UPDATE examples:
Usually, you have 3 cases in which you want to update records :
you know the record identifier (generally the recordID.recordName of the record you want to save: in that case,
you will use methods fetchRecordWithID and then saveRecord
you know there is a unique record to update but you don't know its recordID: in that case, you will use a query with method
performQuery, select the (only) one you need and again saveRecord
you are dealing with many records that you want to update: in that case, you will use a query to fetch them all
(performQuery), and a CKModifyRecordsOperation to save them all.
Case 1 - you know the unique identifier for the record you want to update:
let myRecordName = aUniqueIdentifierForMyRecord
let recordID = CKRecordID(recordName: myRecordName)
database.fetchRecordWithID(recordID, completionHandler: { (record, error) in
if error != nil {
println("Error fetching record: \(error.localizedDescription)")
} else {
// Now you have grabbed your existing record from iCloud
// Apply whatever changes you want
record.setObject(aValue, forKey: attributeToChange)
// Save this record again
database.saveRecord(record, completionHandler: { (savedRecord, saveError) in
if saveError != nil {
println("Error saving record: \(saveError.localizedDescription)")
} else {
println("Successfully updated record!")
}
})
}
})
Case 2 - you know there is a record corresponding to your conditions, and you want to update it:
let predicate = yourPredicate // better be accurate to get only the record you need
var query = CKQuery(recordType: YourRecordType, predicate: predicate)
database.performQuery(query, inZoneWithID: nil, completionHandler: { (records, error) in
if error != nil {
println("Error querying records: \(error.localizedDescription)")
} else {
if records.count > 0 {
let record = records.first as! CKRecord
// Now you have grabbed your existing record from iCloud
// Apply whatever changes you want
record.setObject(aValue, forKey: attributeToChange)
// Save this record again
database.saveRecord(record, completionHandler: { (savedRecord, saveError in
if saveError != nil {
println("Error saving record: \(saveError.localizedDescription)")
} else {
println("Successfully updated record!")
}
})
}
}
})
Case 3 - you want to grab multiple records, and update them all at once:
let predicate = yourPredicate // can be NSPredicate(value: true) if you want them all
var query = CKQuery(recordType: YourRecordType, predicate: predicate)
database.performQuery(query, inZoneWithID: nil, completionHandler: { (records, error) in
if error != nil {
println("Error querying records: \(error.localizedDescription)")
} else {
// Now you have grabbed an array of CKRecord from iCloud
// Apply whatever changes you want
for record in records {
record.setObject(aValue, forKey: attributeToChange)
}
// Save all the records in one batch
var saveOperation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
saveOperation.savePolicy = .IfServerRecordUnchanged // default
saveOperation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordsIDs, error in
if error != nil {
println("Error saving records: \(error.localizedDescription)")
} else {
println("Successfully updated all the records")
}
}
database.addOperation(saveOperation)
}
})
Now, that was a lenghty answer to your question, but your code mixed both a unitary save method with a CKModifyRecordsOperation.
Also, you have to understand that, each time you create a CKRecord, CloudKit will give it a unique identifier (the record.recordID.recordName), unless you provide one yourself. So you have to know if you want to fetch an existing record, or create a new one before calling all these beautiful methods :-)
If you try to create a new CKRecord using the same unique identifier as another one, then you'll most certainly get an error.
I had the same error, but I was already fetching the record by ID as Guto described. It turned out I was updating the same record multiple times, and things were getting out of sync.
I have an update-and-save method that gets called by the main thread, sometimes rapidly.
I'm using blocks and saving right away, but if you're updating records quickly you can arrive in a situation where the following happens:
Fetch record, get instance A'.
Fetch record, get instance A''.
Update A' and save.
Update A'' and save.
Update of A'' will fail because the record has been updated on the server.
I fixed this by ensuring that I wait to update the record if I'm in the midst updating it.

Resources