Swift CloudKit SaveRecord "Error saving record" - ios

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.

Related

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

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.

Is it possible to delete a HealthKit entry from my app?

I'm making an app with HealthKit and want to try to add a swipe to delete on my table view. I know there is a healthStore.delete option, but will this delete from the Health app and how would I know which HKSample to delete from HealthKit.
The HKSample class is an abstract class. Thus you should should never instantiate a HKSample object directly. Instead, you always work with one of the subclasses of HKSample (HKCategorySample, HKQuantitySample, HKCorrelation, or HKWorkout classes) where HKSampleClass1 would be one of the subclasses.
healthStore.deleteObject(HKSampleClass1) { (success: Bool, error: NSError?) -> Void in {
if success () {
//success in deletion
}
}}
In the first step, you need to define your specific key in HKMetadataKeySyncIdentifier before you save your data to apple health.
And then, you can use HKMetadataKeySyncIdentifier to delete specific health data.
let healthKitStore = HKHealthStore()
// SAVE
var meta = [String: Any]()
meta[HKMetadataKeySyncVersion] = 1
meta[HKMetadataKeySyncIdentifier] = "specific key"
let recordSample = HKQuantitySample(type: type, quantity: quantity, start: date, end: date, metadata: meta)
healthKitStore.save(bloodGlucoseSample) { success, error in
if success {
print("saving record to health success")
} else {
print("saving record to health error = \(String(describing: error))")
}
}
// DELETE
let predicate = HKQuery.predicateForObjects(withMetadataKey: HKMetadataKeySyncIdentifier, allowedValues: ["specific key"])
healthKitStore.deleteObjects(of: bloodGlucoseType, predicate: predicate) { success, _, error in
if success {
print("delete health record success")
} else {
print("delete health record error = \(String(describing: error))")
}
}
Yes, calling healthStore.deleteObject() will delete the sample from Health. However, keep in mind that your app may only delete samples that it saved to HealthKit.
You'll need to perform a query to retrieve the samples you want to show to the user. You could use HKSampleQuery or HKAnchoredObjectQuery.

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!

Fetching CloudKit User Record using UserRecordID

The question is to fetch data from User Records when UserRecordID is fetched.
Method to get User ID:
post.creatorUserRecordID?.recordName
My Users Record Type contains columns like username, so, I need to parse them for a concrete user. Is it possible somehow?
If I understood your question and you already have a CKRecordID. All you got to do then is to fetchRecordWithID using this CKRecordID you got.
let publicDatabase = CKContainer.defaultContainer().publicCloudDatabase
publicDatabase.fetchRecordWithID(recordId, completionHandler: { (fetchRecord: CKRecord?, fetchError: NSError?) in
if let error = fetchError
{
// error getting user record, try again
print("-> cloudKitFetchUserRecord - error fetching user record - Error \(error)")
}
else
{
if let record = fetchRecord
{
if record.recordType == CKRecordTypeUserRecord
{
// valid record
print("-> cloudKitFetchUserRecord - fetching user record - valid record found - \(record.recordID.recordName))")
// unwrap your values - on your case username
if let object = record.objectForKey("username") as? Bool
{
// do something with object
}
}
else
{
// not valid record
print("-> cloudKitFetchUserRecord - fetching user record - The record that came back is not a CKRecordTypeUserRecord")
}
}
else
{
// record nil
print("-> cloudKitFetchUserRecord - fetching user record - fetch record returned nil")
}
}
})
It is simpler now using async/await in Swift 5
func getUserID() async {
let container = CKContainer(identifier: "iCloud.com.XXX.XXXX")
// CKContainer.defaultContainer().publicCloudDatabase // or default public container
do {
let userRecordID = try await container.userRecordID()
print("recordName: \(userRecordID.recordName)")
}
catch {
print("Error: \(error)")
}

How to prevent duplicate entry on parse?

I' trying to save song info to parse, but if the song already exist in parse I want my code just do nothing.
I've tried this code below:
var Music = PFObject(className:"Musics")
var query = PFQuery(className:"Musics")
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]?, error: NSError?) -> Void in
if error == nil {
// The find succeeded.
println("Successfully retrieved \(objects!.count) scores.")
// Do something with the found objects
if let objects = objects as? [PFObject] {
for object in objects {
var songTitle = object.objectForKey("songTitle") as? String
if songTitle != title {
Music["createdBy"] = PFUser.currentUser()
Music["songTitle"] = title
Music["albumCover"] = imageFile
Music["songArtist"] = artist
Music.saveInBackgroundWithBlock {
(success: Bool, error: NSError?) -> Void in
if (success) {
println("succeed")
} else {
// There was a problem, check error.description
println("error jeh")
}
}
}else{
println("song already exist")
}
}
}
} else {
// Log details of the failure
println("Error: \(error!) \(error!.userInfo!)")
}
}
the code above give below result on log:
Successfully retrieved 4 scores.
song already exist
Successfully retrieved 4 scores.
song already exist
Successfully retrieved 4 scores.
song already exist
Successfully retrieved 4 scores.
song already exist
succeed
succeed
succeed
succeed
succeed
succeed
succeed
succeed
succeed
succeed
succeed
succeed
Why my for loop , looping more than the Objects.count? and how can I prevent dupiclate entry on parse?
give me any advice, doesn't matter in obj c or swift
I suggest to implement a simple beforeSave trigger, on Parse Cloud code, in order to check if the new entry song already exist (basically you're going to make one or more field uniques. For example:
Parse.Cloud.beforeSave("Musics", function(request, response) {
var newEntrySong = request.object;
var querySongs = new Parse.Query("Musics");
querySongs.equalTo("title", newEntrySong.get("title"));
querySongs.equalTo("description", newEntrySong.get("description"));
// this could be a sort of signature for your song, to make more unique (skipping spaces and new lines for example)
querySongs.equalTo("md5Title", newEntrySong.get("md5Title"));
querySongs.first({
success: function(temp) {
response.error({errorCode:123,errorMsg:"Song already exist!"});
},
error: function(error) {
response.success();
}
});
});
Hope it helps.

Resources