Saving Modified Data in CloudKit - ios

I have been testing out CloudKit as i wish to release an app using it when the release of iOS8 occurs. It seems simple enough to save data using the code below:
CKRecordID * recordID = [[CKRecordID alloc] initWithRecordName:#"basicRecord"];
CKRecord * record = [[CKRecord alloc] initWithRecordType:#"basicRecordType" recordID:recordID];
[record setValue:#"defaultValue" forKey:#"defaultKey"];
CKDatabase *database = [[CKContainer defaultContainer] publicCloudDatabase];
[database saveRecord:record completionHandler:^(CKRecord *record, NSError *error) {
if (error) {
NSLog(#"Error: %#", error);
} else {
NSLog(#"Record Saved!");
}
}];
and I receive no errors from this. However, if i try to run the code again, maybe because i have changed the record value to
[record setValue:#"newValue" forKey:#"defaultKey"];
I receive an error which begs the question, how do i go about saving a modified piece of data. After all, this is a fundamental part of saving things to the cloud. The error is below and any help would be greatly appreciated, don't hesitate to ask for further information.
Error: <CKError 0x17024afb0: "Server Record Changed" (14/2017); "Error saving record <CKRecordID: 0x144684a80; basicRecord:(_defaultZone:__defaultOwner__)> to server: (null)"; uuid = 182C497F-966C-418A-9E6A-5563BA6CC6CD; container ID = "iCloud.com.yourcompany.CloudKit">

This error is probably because saveRecord: works only for new records or records that are newer than the version on the server:
This method saves the record only if it has never been saved before or if it is newer than the version on the server. You cannot use this method to overwrite newer versions of a record on the server. CKDatabase docs
The recommended approach to modify an existing record (or set of records) is to use a CKModifyRecordsOperation set with the desired savePolicy to deal with conflicts:
After modifying the fields of a record, use this type of operation object to save those changes to a database. (...)
When saving records, the value in the savePolicy property determines how to proceed when conflicts are detected on the server. CKModifyRecordsOperation docs

From the docs of CKRecord:
New records exist only in memory until you explicitly save them to iCloud.
When you set the new value [record setValue:#"newValue" forKey:#"defaultKey"]; you have already saved the record, making it invalid.
You can use CKModifyRecordsOperation and in most situations it might be preferrable but you don't have to. Just fetch your data using a fresh CKRecord, then feed that record into saveRecord: as described here.

After you save the record, fetch it so that the retured record will then have the RecordID that Cloudkit added
Then on that same fetched record, use setValue to change the data you want to change
Then you can use CFModifyRecordsOperation
In the example below, cachedCKRecordsServiceCenter contains the fetched records from cloudkit and those records have the CloudKit RecordID's in them......
//find this service center in the cached records
for (_,serviceCenter) in (theModel?.cachedCKRecordsServiceCenter.enumerated())! //is data for logged in Co ONLY with NO Co name attached
{
let name = serviceCenter["name"] as! String
returnValue = "Try Again"
if name == displayedRecordName
{
serviceCenter.setValue(displayedRecordName! + "_" + (theModel?.companyName)!, forKey: "name") //db values have Co name appended
serviceCenter.setValue(label2Text.text, forKey:"street1")
serviceCenter.setValue(label3Text.text, forKey:"street2")
serviceCenter.setValue(label4Text.text, forKey:"city")
serviceCenter.setValue(label5Text.text, forKey:"state")
serviceCenter.setValue(label6Text.text, forKey:"zip")
serviceCenter.setValue(label7Text.text, forKey:"phone")
serviceCenter.setValue(label8Text.text, forKey:"email")
serviceCenter.setValue(label9Text.text, forKey:"note")
let saveRecordsOperation = CKModifyRecordsOperation()
var ckRecordsArray = [CKRecord]()
// set values to ckRecordsArray
ckRecordsArray.append(serviceCenter)
saveRecordsOperation.recordsToSave = ckRecordsArray
saveRecordsOperation.savePolicy = .ifServerRecordUnchanged
appDelegate.locked = true
saveRecordsOperation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordIDs, error in
if error != nil {
// Really important to handle this here
////////print("ERROR: Unable to update Driver Location: Error= \(error)")
self.returnValue = "ERROR: Unable to update Driver Location: ERROR = \(error)"
self.appDelegate.locked=false
}
else
{
////print("Successfully updated Service Center")
self.appDelegate.locked=false
self.returnValue = "Successfully updated Service Center"
self.appDelegate.locked=false
//reget the data into the cach
self.theModel?.fetchServiceCenterFromCloudKit1()
}
}
CKContainer.default().publicCloudDatabase.add(saveRecordsOperation)
}
}

Related

Identify Type of Record Change when Retrieving Changed CloudKit Records

I am attempting to complete an app using CloudKit synchronization and local CoreData. Most of the operations work as expected but I cannot find the methodology for determining the type of change that is reported by CloudKit. I get the changed records, but I need to know if the change was an edit, a new record or a deletion. Any guidance would be appreciated.
Here's the piece of code that I thought could be configured to identify the type of edit I would need to make to CoreData. Xcode 10.2.1 iOS 12.2 Swift (Latest)
func fetchZoneChangesInZones( _ zones : [CKRecordZone.ID], _ completionHandler: #escaping (Error?) -> Void) {
var fetchConfigurations = [CKRecordZone.ID : CKFetchRecordZoneChangesOperation.ZoneConfiguration]()
for zone in zones {
if let changeToken = UserDefaults.standard.zoneChangeToken(forZone: zone) {
let configuration = CKFetchRecordZoneChangesOperation.ZoneConfiguration(previousServerChangeToken: changeToken, resultsLimit: nil, desiredKeys: nil)
fetchConfigurations[zone] = configuration
}//if let changeToken
}//for in
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zones, configurationsByRecordZoneID: fetchConfigurations)
operation.fetchAllChanges = true
var changedPatients = [CKRecord]()
var changedCategory1s = [CKRecord]()
//I thought that I should be able to query for the change type here and make separate arrays for each change type
operation.recordChangedBlock = { record in
if record.recordType == "Patient" {
changedPatients.append(record)
}
}//recordChangedBlock
operation.fetchRecordZoneChangesCompletionBlock = { [weak self] error in
for record in changedPatients {
//my actions here - need to choose new, changed or delete
self!.saveCKRecordToCoreData(record: record)
}//for record in
completionHandler(error)
}//fetchRecordZoneChangesCompletionBlock
operation.recordZoneFetchCompletionBlock = { recordZone, changeToken, data, moreComing, error in
UserDefaults.standard.set(changeToken, forZone: recordZone)
}//recordZoneFetchCompletionBlock
privateDatabase.add(operation)
}//fetchZoneChangesInZones
I'm not that good in swift but I will post in objective c so you can convert this into swift
First things first, if you want to notify if a record has been edited, deleted or created you need register for push notifications.
Then Subscribe to update add this block in didFinishLaunchingWithOptions
- (void)subscribeToEventChanges
{
BOOL isSubscribed = [[NSUserDefaults standardUserDefaults] boolForKey:#"subscribedToUpdates"];
if (isSubscribed == NO) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"TRUEPREDICATE"];
CKQuerySubscription *subscription = [[CKQuerySubscription alloc] initWithRecordType:#"Patient" predicate:predicate options:CKQuerySubscriptionOptionsFiresOnRecordCreation | CKQueryNotificationReasonRecordDeleted | CKQueryNotificationReasonRecordUpdated];
CKNotificationInfo *CKNotification=[[CKNotificationInfo alloc]init];
CKNotification.shouldSendContentAvailable=YES;
CKNotification.soundName=#"";
subscription.notificationInfo=CKNotification;
CKDatabase *publicDatabase = [[CKContainer containerWithIdentifier:#"your container identifir"] privateCloudDatabase];
[publicDatabase saveSubscription:subscription completionHandler:^(CKSubscription * _Nullable subscription, NSError * _Nullable error) {
if (error) {
// Handle here the error
} else {
// Save that we have subscribed successfully to keep track and avoid trying to subscribe again
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:#"subscribedToUpdates"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}];
}
}
You will get notified in didReceiveRemoteNotification
Here is a piece of code
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
CKNotification *cloudKitNotification = [CKNotification notificationFromRemoteNotificationDictionary:userInfo];
if (cloudKitNotification.notificationType == CKNotificationTypeQuery) {
CKQueryNotification *queryNotification = (CKQueryNotification *)cloudKitNotification;
if (queryNotification.queryNotificationReason == CKQueryNotificationReasonRecordDeleted) {
// If the record has been deleted in CloudKit then delete the local copy here
} else {
// If the record has been created or changed, we fetch the data from CloudKit
CKDatabase *database;
if (queryNotification.databaseScope) {
database = [[CKContainer containerWithIdentifier:#"your container identifier"] privateCloudDatabase];
}
[database fetchRecordWithID:queryNotification.recordID completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
if (error) {
// Handle the error here
} else {
if (queryNotification.queryNotificationReason == CKQueryNotificationReasonRecordUpdated) {
// Use the information in the record object to modify your local data
}else{
// Use the information in the record object to create a new local object
}
}
}];
}
}
}
The solution is a separate method on the version of the operation that is being used. I was already getting notifications, I just could not tell whether they were update, create or delete. Update and create can be handled simply by searching core data for the recordName(which is a UUID). If found, then edit, if not create. The issue is deletion - using the fetchRecordZoneChangesCompletionBlock could not identify deletions. However, the operation family has a method just to report deletions - operation.recordWithIDWasDeletedBlock. I modified the former code and added the deletion code as shown below.
My single database subscription covers the entire private database so it is not necessary to subscribe to every record type.
operation.fetchRecordZoneChangesCompletionBlock = { error in
for record in changedPatients {
//search for the record in coredata
if self.isSingleCoreDataRecord(ckRecord: record) {
//if found - then modify
self.saveUpdatedCloudKitRecordToCoreData(record: record)
} else {
//else add new
self.saveCKRecordToCoreData(record: record)
}
}//for record in
completionHandler(error)
}//fetchRecordZoneChangesCompletionBlock
operation.recordWithIDWasDeletedBlock = { (recordID, recordType) in
//delete the core data record here
let ckRecordToDelete = CKRecord(recordType: recordType, recordID: recordID)
self.removeOnePatientRecordFromCoreData(ckRecord: ckRecordToDelete)
}//recordWithIDWasDeletedBlock

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.

CloudKit Error Differentiation

I need some help to learn how to properly handle errors when fetching records via CloudKit. Currently I have an app that saves numerous records in the cloud, and will load them at launch. I have been referencing the records using a CKReference, and anytime I save the reference I use the CKReferenceAction.DeleteSelf option. A problem I've encountered periodically is that when a referenced record is deleted, sometimes there can be a significant amount of time before the reference deletes itself. This has caused me to occasionally come across the situation where my app has fetched a CKReference for a record that no longer exists. I'm able to manually find out when this happens just by inserting print(error!) in my error handler. What I would like to know is how I can add some code to detect this specific error i.e. if error.localizedDescription == ??? {.
Here is the basic code I'm using for the fetch:
let fetch = CKFetchRecordsOperation(recordIDs: recordIDs)
fetch.perRecordCompletionBlock = { (record:CKRecord?, recordID:CKRecordID?, error: NSError?) in
if error != nil {
// Error Line A (See below)
print("ERROR! : \(error!.localizedDescription)")
// Error Line B (See below)
print("ERROR: \(error!)")
}
else if let record = record {
// Record was found
}
}
if let database = self.privateDatabase {
fetch.database = database
fetch.start()
}
And then when it tries to fetch the non-existent record, here is the error message that prints out in the compiler window:
a) ERROR! : Error fetching record <CKRecordID: 0x10025b290; dbbda7c3-adcc-4271-848f-6702160ea34f:(_defaultZone:__defaultOwner__)> from server: Record not found
b) ERROR: <CKError 0x125e82820: "Unknown Item" (11/2003); server message = "Record not found"; uuid = (removed); container ID = "(removed)">
Above in error line B, where it says CKError 0x125e82820:, can I use this to create an if statement to check for this specific error type? I really could use any help finding a way to resolve this issue properly when it happens. I have set up some loading structure for my app, and when it thinks there is a record it needs to find, but can't, it screws up my loading process. I would really appreciate any help I can get, I assume it's an easy solution, but apparently not one I've been able to find. Thank you!
UPDATE -
Thanks to #AaronBrager, I was able to find the correct solution. You can verify the error code to match it to any specific error, and the domain to make sure it's a CKError. Here is the solution that works for me:
let fetch = CKFetchRecordsOperation(recordIDs: recordIDs)
fetch.perRecordCompletionBlock = { (record:CKRecord?, recordID:CKRecordID?, error: NSError?) in
if error != nil {
if error!.code == CKErrorCode.UnknownItem.rawValue && error!.domain == CKErrorDomain {
// This works great!
}
}
else if let record = record {
// Record was found
}
}
if let database = self.publicDatabase {
fetch.database = database
fetch.start()
}
You should be able to uniquely identify an error's cause by inspecting its domain and code variables. Same domain and code, same problem. And unlike localizedDescription, it won't change between users.

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.

Cannot update object that was never inserted

I create an category object and save it:
NSManagedObjectContext *managedObjectContext = [[FTAppDelegate sharedAppDelegate] managedObjectContext];
_category = (Category *)[NSEntityDescription
insertNewObjectForEntityForName:#"Category"
inManagedObjectContext:managedObjectContext];
NSError *error = nil;
[managedObjectContext save:&error];
if (error) {
NSLog(#"error saving: %#",error);
}
then edit the name of the category object and save again.
_category.name = _nameTextField.text;
NSManagedObjectContext *managedObjectContext = [[FTAppDelegate sharedAppDelegate] managedObjectContext];
NSError *error = nil;
[managedObjectContext save:&error];
if (error) {
NSLog(#"error saving: %#",error);
}
and get this error:
2013-01-12 17:53:11.862 instacat[7000:907] Unresolved error Error Domain=NSCocoaErrorDomain Code=134030 "The operation couldn’t be completed. (Cocoa error 134030.)" UserInfo=0x2027b300 {NSAffectedObjectsErrorKey=(
"<Category: 0x1ed43cf0> (entity: Category; id: 0x1ed52970 <x-coredata://68E5D7B6-D461-4962-BC07-855349DB3263-7000-00000141BAB4C399/Category/tE8AB2F2E-C14C-4E93-8343-CC245B7726622> ; data: {\n categoryId = nil;\n isPrivate = 0;\n name = techies;\n users = (\n );\n})"
), NSUnderlyingException=Cannot update object that was never inserted.}, {
NSAffectedObjectsErrorKey = (
"<Category: 0x1ed43cf0> (entity: Category; id: 0x1ed52970 <x-coredata://68E5D7B6-D461-4962-BC07-855349DB3263-7000-00000141BAB4C399/Category/tE8AB2F2E-C14C-4E93-8343-CC245B7726622> ; data: {\n categoryId = nil;\n isPrivate = 0;\n name = techies;\n users = (\n );\n})"
);
NSUnderlyingException = "Cannot update object that was never inserted.";
}
Thank you for your time and consideration.
I am using the AFIncrementalStore.
How about something like this:
self.category.name = self.nameTextField.text;
NSManagedObjectContext *managedObjectContext = [[FTAppDelegate sharedAppDelegate] managedObjectContext];
if(![self.category isInserted])
{
[managedObjectContext insertObject:self.category];
}
NSError *error = nil;
[managedObjectContext save:&error];
if (error) {
NSLog(#"error saving: %#",error);
}
Basically check the object is it has been inserted before, if not, insert it and then save the context.
When you update an object, you can't use insertNewObjectForEntityForName, you need to first save your object, then call something like
[self.managedObjectContext refreshObject:_category mergeChanges:YES]
Then use managedObjectContext save again.
This is the difference in direct SQL as "INSERT" and "UPDATE".
Your object is loosing the managedObjectContext. Either use
self.managedObjectContext
or refetch the object in
[[FTAppDelegate sharedAppDelegate] managedObjectContext]
and edit the refetched object and then save it.
I have the same error but different and rare scenario, it happens once in almost 100 attempts. Find my problem below:
I have 2 NSManagedObjects in core data model:
1- Lead
2- LeadAttirbute
Lead has 1-M relationship with LeadAttribute.
There is a form that inputs for lead and refresh(create new lead) the form after submitting a lead. If i keep on submitting the leads then at a stage, [managedObjectContext save:&error]; starts giving below error:
Domain=NSCocoaErrorDomain Code=134030 "The operation couldn’t be completed. (Cocoa error 134030.)" UserInfo=0x1f251740 {NSAffectedObjectsErrorKey=(
" (entity: LeadInfoAttribute; id: 0x1f2eb920 ; data: {\n attributeId = 0;\n lead = nil;\n optional = nil;\n orderId = 0;\n title = nil;\n value = Bjjbjp;\n})"
), NSUnderlyingException=Cannot update object that was never inserted.}
And it keeps on giving the same error until i dont terminate and re-launch the app. I'm not able to update anything in core data model after this error occur, So my questions are:
1- Can we remove the fault state of core data? i.e to capture and delete the object that is creating trouble before making the save call again.
2- What could be the possible reasons for this issue? Since its very rare and can't reproduce this everytime.
I've just run into this issue and in my case the problem was following:
1) create new managed object in context A
2) save the context A
3) retrieve this object by objectID from context B
4) make changes on managed object and save the context B
Normally it wouldn't be a problem, but in this case the context A is child context and therefore doesn't save to persistent store (just to parent context, which isn't the context B). So when fetch for managed object is done from context B, context doesn't have this object. When changes are made, context tries to save them anyway...and thats when this error occurs. In some cases (as #Trausti Thor mentioned) the refreshObject:mergeChanges: method could help (it passes the data to another context).
In Your case I'll check if:
1) managed object context from [[FTAppDelegate sharedAppDelegate] managedObjectContext] returns always the same context
2) when you save the category, check if it was really saved to persistent store (self.category.objectID.isTemporaryID == NO)
NOTE:
The second point is more important, because if you look carefully, your category object still has temporary objectID, that means it's not persisted.
What I think it's happening is that you are not getting the right NSManagedObjectContext.
Think about it as a session. So when you update you are not getting the right session and so your object doesn't exist there.
Before doing the second save try to find your object on that NSManagedObjectContext.
If you need further help please describe what happens between the creation and the update.
Getting the wrong NSManagedObjectContext can be due to bad code on the AppDelegate or accessing from another thread other than the main thread.

Resources