Modify data with CloudKit - ios

I have data I'd like to modify in CloudKit. I've found this question (Saving Modified Data in CloudKit) and it points to CKModifyRecordsOperation, but being new to this I'm looking for more guidance. I'm setting my object like so:
[object setValue:number forKey:#"total"];
If I'm only modifying one record and not all do I still call CKModifyRecordsOperation?
Any clues to how this is done?
I've been using [self.cloudManager saveRecord:object]; but with modifying the record this isn't working.

You can fetch, modify, and save changes you make to individual records.
The code snippet below shows how to fetch an Artwork record, changes the date attribute value, and saves it to the database.
// Fetch the record from the database
CKDatabase *publicDatabase = [[CKContainer containerWithIdentifier:containerIdentifier] publicCloudDatabase];
CKRecordID *artworkRecordID = [[CKRecordID alloc] initWithRecordName:#"115"];
[publicDatabase fetchRecordWithID:artworkRecordID completionHandler:^(CKRecord *artworkRecord, NSError *error) {
if (error) {
// Error handling for failed fetch from public database
}
else {
// Modify the record and save it to the database
NSDate *date = artworkRecord[#"date"];
artworkRecord[#"date"]; = [date dateByAddingTimeInterval:30.0 * 60.0];
[publicDatabase saveRecord:artworkRecord completionHandler:^(CKRecord *savedRecord, NSError *saveError) {
// Error handling for failed save to public database
}];
}
}];
Consider to read this article for more detailed information.

Related

Deleting a CKRecord is really confusing

So I'm having a really weird problem, I suppose I either dont understand how CloudKit works under the hood or I encountered a bug in CloudKit.
So, the issue looks like this:
App initial state:
I have 5 "Package" records, lets call them A, B, C, D, E.
User action
The user will delete "Package" record E and at some later point in time he will press a refresh button which will fetch all current "Package" records from the cloud.
The problem
When the user presses the refresh button, the app will basically look at the existing locally stored "Package" records, and will create a CKQuery with a predicate that should fetch any other records that do not exist locally. The next step is basically calling the [database performQuery: inZoneWithID:completionHandler:] method.
The surprise shows up when I get the results, which contain the "Package" record E that the user previously deleted.
This doesnt seem to be right to me...
The steps I took to debug:
Right after deleting the "Package" record E, I created a CKFetchRecordsOperation and tried to fetch the deleted record. The result was as expected: I got a "Record not found". I'm cool here.
Thinking there might be some delays on the server side, I put a dispatch_after block and launched the same fetch operation I did in point 1 but just after 30 seconds. The result was still as expected: I got the "Record not found" error.
Performed the same test as I did in point 2 but with a delay of 100 seconds and ... surprise, the CKFetchRecordsOperation operation returned the deleted record E package. The weird thing is that somethings it will still return an error, but sometimes will just plainly return the deleted object.
And now the really weird part: This does not happen with record A, B, C and D, the single difference between all theses records are their names. This does not make any sense.
I filled a bug report and the reply I got was this:
This is correct behavior. Queries are eventually consistent so the deletes may not immediately be reflected when querying. Fetching the deleted record by ID via a CKFetchRecordsOperation should return a CKErrorUnknownItem right away.
While this is partially true, this does not seems to be the case with what I'm seeing.
Code
Deleting the record E with name DS2000330803AS, the check CKFetchRecordsOperation operation returns an error with Record not found. All good here.
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDB = [container privateCloudDatabase];
CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName: #"DS2000330803AS"];
CKModifyRecordsOperation *operation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave: nil recordIDsToDelete: #[recordID]];
operation.database = privateDB;
[operation setModifyRecordsCompletionBlock:^(NSArray<CKRecord *> * _Nullable savedRecords,
NSArray<CKRecordID *> * _Nullable deletedRecordIDs,
NSError * _Nullable error) {
CKFetchRecordsOperation *fetchOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs:#[recordID]];
fetchOperation.database = privateDB;
[fetchOperation setPerRecordCompletionBlock:^(CKRecord * _Nullable record, CKRecordID * _Nullable recordID, NSError * _Nullable error){
NSLog(#"Error: %#", error.localizedDescription);
}];
}];
Placing a NSTimer in my VC just to test the Record deletion, this piece of code will return the deleted record:
[NSTimer scheduledTimerWithTimeInterval:100 repeats:NO block:^(NSTimer * _Nonnull timer) {
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDB = [container privateCloudDatabase];
CKRecordID *recordID = [[CKRecordID alloc] initWithRecordName:#"DS2000330803AS"];
CKFetchRecordsOperation *fetchOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs: #[recordID]];
fetchOperation.database = privateDB;
[fetchOperation setPerRecordCompletionBlock:^(CKRecord * _Nullable record, CKRecordID * _Nullable recordID, NSError * _Nullable error){
NSLog(#"Error: %#", error.localizedDescription);
}];
[privateDB addOperation: fetchOperation];
}];
The piece of code that fetches all the existing records by pressing a refresh button which the user can press at any time. I simplified this code a bit to just expose the problem, basically the performQuery returns the DS2000330803AS record, and for the sake of testing my sanity, I'm adding a CKFetchRecordsOperation to fetch the record again, which of course does return it without any issues.
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *privateDB = [container privateCloudDatabase];
NSPredicate *predicate = [NSPredicate predicateWithValue: YES];
CKQuery *query = [[CKQuery alloc] initWithRecordType:#"Package" predicate:predicate];
[privateDB performQuery:query
inZoneWithID:nil completionHandler:^(NSArray<CKRecord *> * _Nullable results, NSError * _Nullable error) {
[results enumerateObjectsUsingBlock:^(CKRecord * _Nonnull record, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(#"Record ID: %#", record.recordID);
CKFetchRecordsOperation *fetchOperation = [[CKFetchRecordsOperation alloc] initWithRecordIDs: #[record.recordID]];
fetchOperation.database = privateDB;
[fetchOperation setPerRecordCompletionBlock:^(CKRecord * _Nullable record, CKRecordID * _Nullable recordID, NSError * _Nullable error){
NSLog(#"Error: %#", error.localizedDescription);
}];
[privateDB addOperation: fetchOperation];
}];
}];
Other notes: I removed and commented pretty much everything related to CloudKit and the above code is the only one that interacts with CloudKit. I'm testing with a single device at the moment.
I know the CKQuery can have a better NSPredicate, but now I try to understand why I have this issue.
P.s. When I added the first implementation of CloudKit to my app, I tried to keep it as simple as possible, without any fancy syncing stuff. It worked just fine for a year, then I started getting reports from my users that they cannot delete some records in production.
Any hints guys on how I should further debug this?
Thank you!
I think you are mixing up record Type and record Name(String of CKRecordID). Name is assigned by CloudKit(Typically) and type is set by you. I would bet it was auto assigned but I would have to see how the record was saved. It would be telling to see a screenshot of your CloudKit Dashboard.
In your block of code in
1) you are trying to delete the record name of some record using the record type. That is why you get the error "Record not found"
2) Same as you are still using Record Type and not record name
3) Fetches the record because it is actually using the assigned record.recordID.
This is my gut on the situation. As far as deleting and refreshing please see my answer on stitching records to keep UI and database in sync.

IOS/Objective-C: Edit managed object after it is inserted into core data

After inserting a new object in core-data representing a new item and then doing a server call on a background thread to insert it into a database on the back end, I retrieve a new id from the server that I would like to insert back into Core-Data.
Can anyone recommend a save new item and call server without losing the new item in the ManagedObjectContext. I would like to make the edit to the managed object without having to do an entire NSFetch with an NSPredicate to find it again but right now, it seems to be disappearing.
Here is my code to store the object locally in Core Data and then go go server. When I get back, however, I don't know how to access this new record.
NSEntityDescription *entity = [NSEntityDescription entityForName:#“Item” inManagedObjectContext:self.managedObjectContext];
// Initialize Record
NSManagedObject *record = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];
// Populate Record
[record setValue:name forKey:#“name”];
// Save Record
NSError *error = nil;
if ([self.managedObjectContext save:&error]) {
//code to set up background request
[NSURLConnection sendAsynchronousRequest:rq queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *rsp, NSData *data, NSError *err) {
if (err) {
} else {
NSDictionary *jsonResults = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
NSInteger insertID = [jsonResults[#"response"][#"insert_id"] integerValue];
self.newid = *(&(insertID));
dispatch_async(dispatch_get_main_queue(), ^{
// //NEED WAY TO INSERT INTO CORE DATA WITHOUT DOING FULL BLOWN FETCH BUT RECORD SEEMS TO BE GONE
});
}
}];
}
2 options, kinda lazy and 'correct'
Assuming that this all originates on the main thread, so record is inserted into the main context, you can just capture record in the block and update it. Don't make any other changes to it on any other thread. Also, don't delete it or the context before this operation is finished.
Better, get the objectID from the new record, make sure it's a permanent id and not temporary (see obtainPermanentIDsForObjects:error:). Now for whichever thread you're working on you can request, from the appropriate context, performing a block if you need to, the objectRegisteredForID:.

Azure Mobile Services - Duplicate item after synchronization

I am using Azure Mobile Service as a backend for an iOS application. I have set up everything to work with offline sync which allows me to view, add, or modify data even when there is no network connection. I am running into a problem when I add a new object into a table. The add works well locally but when I synchronize data it creates a duplicate item on the local database with a slightly different objectId. The created item is not duplicated on the server side.
Here's how I am setup. By the way, thanks to #TheBasicMind for posting this model.
Here's a link to his explanation of the model: enter link description here
Here's what I do to setup the sync context and sync table:
// Initialize the Mobile Service client with your URL and key
MSClient *client = self.hpc.client;
NSManagedObjectContext *context = self.hpc.syncContext;
MSCoreDataStore *store = [[MSCoreDataStore alloc] initWithManagedObjectContext:context];
client.syncContext = [[MSSyncContext alloc] initWithDelegate:syncDelegate dataSource:store callback:nil];
// Add a Mobile Service filter to enable the busy indicator
self.client = [client clientWithFilter:self];
// Create an MSSyncTable instance to allow us to work with the Athlete table
self.syncAthleteTable = [self.client syncTableWithName:#"Athlete"];
Here's how I add a record for the moment:
NSDictionary *newItem = #{#"firstname": firstname, #"lastname": lastname, #"laterality" : laterality};
[self.athletesService addItem:newItem completion:^{
NSLog(#"New athlete added");
}];
-(void)addItem:(NSDictionary *)item completion:(CompletionBlock)completion
{
// Insert the item into the Athlete table
[self.syncAthleteTable insert:item completion:^(NSDictionary *result, NSError *error)
{
[self logErrorIfNotNil:error];
// Let the caller know that we finished
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}];
}
The add works as expected and it is added in a UITableView as I have an NSFetchedResultsController listening on my Main Context.
Here's where the problem occurs. When I synchronize data with the server using this function:
-(void)syncData:(CompletionBlock)completion
{
// push all changes in the sync context, then pull new data
[self.client.syncContext pushWithCompletion:^(NSError *error) {
[self logErrorIfNotNil:error];
[self pullData:completion];
}];
}
-(void)pullData:(CompletionBlock)completion
{
MSQuery *query = [self.syncAthleteTable query];
// Pulls data from the remote server into the local table.
// We're pulling all items and filtering in the view
// query ID is used for incremental sync
[self.syncAthleteTable pullWithQuery:query queryId:#"allAthletes" completion:^(NSError *error) {
[self logErrorIfNotNil:error];
[self refreshDataOnSuccess:completion];
}];
}
- (void) refreshDataOnSuccess:(CompletionBlock)completion
{
MSQuery *query = [self.syncAthleteTable query];
[query readWithCompletion:^(MSQueryResult *results, NSError *error) {
[self logErrorIfNotNil:error];
NSLog(#"Data that pulled from local store: ");
for ( NSDictionary *dict in results.items ) {
NSLog(#"%# %#", [dict objectForKey:#"firstname"], [dict objectForKey:#"lastname"] );
}
// Let the caller know that we finished
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}];
}
After the synchronization the NSFetchedResultsChangeInsert is called a second time for the same record with a slightly different objectID. Here's an example of the first and second objectIDs:
tD7ADE77E-0ED0-4055-BAF6-B6CF8A6960AE9
tD7ADE77E-0ED0-4055-BAF6-B6CF8A6960AE11
I am stuck here.
Any help is highly appreciated. Thank you!
In the past, when I've seen this happen, its because the "id" field the client is sending was being changed or ignored by the server logic.
Locally the store finds the object in core data using that field, so a change to it could result in the client SDK thinking it needs to insert a new object and not update an existing one.
One easy way to confirm this, is by using the tableOperation:complete: method on the data delegate and comparing the "id" column between the item originally and that being returned by operation execute.

cloudKit: CKFetchRecordChangesOperation in public database

I building a iOS app using cloudKit. I'm trying to make a batch fetch of the data in cloudKit getting the deltas between the device and cloudKit but it seems like CKFetchRecordChangesOperation doesn't work in public database. Does my only option option is CKQuery to fetch my data ? for example:
CKContainer *container = [CKContainer containerWithIdentifier:containerID];
CKDatabase *publicDatabase = [container publicCloudDatabase];
CKQuery *query = [[CKQuery alloc] initWithRecordType:recordType
predicate:[NSPredicate predicateWithFormat:#"TRUEPREDICATE"]];
CKQueryOperation *queryOp = [[CKQueryOperation alloc] initWithQuery:query];
queryOp.desiredKeys = #[#"record.recordID.recordName"];
queryOp.recordFetchedBlock = ^(CKRecord *record)
{
// do something...
};
queryOp.queryCompletionBlock = ^(CKQueryCursor *cursor, NSError *error)
{
// do something else...
};
queryOp.resultsLimit = CKQueryOperationMaximumResults;
[publicDatabase addOperation:queryOp];
I'll really appreciate your help.
The apple documentation for CKFetchRecordChangesOperation states:
recordZoneID : The zone containing the records you want to fetch. The zone can be a
custom zone. Syncing the default zone is not supported.
This means that it will not work on the public database since than only supports the default zone.
The correct way to achieve the same functionality would be by creating subscriptions for the data you need and retrieve that data using the CKFetchNotificationChangesOperation. Of course you could also just execute some CKQuery commands, but then you would probably often fetch data or execute queries that you don't need.

Core Data Entity Created But Attributes Not Saving

I have a relatively simple entity. When I create it, set its attributes, and save it, it saves successfully. I can later retrieve it and it is not nil and I get a successful save message from MagicalRecord.
When I retrieve it and try to access any attribute though the attribute is nil. The entity itself is fine but the attributes are all nil. I have checked they are all definitely set correctly before I save.
I haven't encountered this problem before. Why could it be occurring?
NB: This doesn't happen every time. Most times I call the method to create and save this entity it can later be retrieved without any issues. The problem is intermittent but possible to replicate on every run.
Code:
Entity1 *entity1 = [Entity1 MR_createEntityInContext:localContext];
[entity1 setUpEntity:myobject];
EntityChild *entityChild=[EntityChild MR_createEntityInContext:localContext];
[entityChild setUpEntityChild:entity.child withContext:localContext];
[entityChild setEntity1:entity1];
[localContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
}];
Update:
If I look in the sqlite database and search for the entity it actually doesn't exist at all. So MagicalRecord tells me it saves, CoreData lets me retrieve a non-nil object (albeit with nil attributes) but no record exists in the database.
I did not understand ur code standards. As I am new to IOS Development. I Used below code for retrieving.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entityRef = [NSEntityDescription entityForName:#"Entity1" inManagedObjectContext:appDelegate.managedObjectContext];//localContext
[fetchRequest setEntity:entityRef];
NSError *error=nil;
NSArray *detailsArray = [appDelegate.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if (error) {
NSLog(#"Unable to execute fetch request.");
NSLog(#"%#, %#", error, error.localizedDescription);
}
Saving the data
NSManagedObjectContext *context = [appDelegate managedObjectContext];//localContext
NSManagedObject *objectRef = [NSEntityDescription
insertNewObjectForEntityForName:#"Entity1" inManagedObjectContext:context];
[objectRef setValue:#"IOS" forKey:#"Name"];
[objectRef setValue:#"positive" forKey:#"Attitude"];
NSError *error;
if (![context save:&error]) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
Hope it helps you...!
Ok, I got to the bottom of this. It wasn't a problem with the code when I did the save. It was actually a problem with some code in another class that was retrieving the data from the wrong context. When I changed the context it worked correctly.
I'm still not sure why this only happened occasionally and not every time the code was run but it's working now.
Thanks for your help anyway everyone.

Resources