Continuing CloudKit fetch in background - ios

My goal is to be able to save a record using CloudKit when my app gets backgrounded. I first fetch the record I'm trying to modify, modify it, then save it. I'm making this call from my app delegate's applicationDidEnterBackground:
CKDatabase *db = [[CKContainer defaultContainer] privateCloudDatabase];
CKRecordID *recordId = [[CKRecordID alloc] initWithRecordName:#"record_a"];
[db fetchRecordWithID:recordId completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
// Doesn't get here until I foreground the app
if (!record) // Doesn't exist yet so create it
{
record = [[CKRecord alloc] initWithRecordType:#"SaveData" recordID:recordId];
}
NSURL *fileURL = [NSURL fileURLWithPath:#"path/to/file"];
CKAsset *saveAsset = [[CKAsset alloc] initWithFileURL:fileURL];
record[#"saveAsset"] = saveAsset;
[db saveRecord:record completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable saveError) {
// Doesn't get here until I foreground
}];
}];
The completion block for the fetchRecordWithID call doesn't get fired until I foreground the app again, meaning the saveRecord: call doesn't get called until then either.
I've tried using the CKOperation version of fetch and save with modified qualityOfService and queuePriority properties to no avail. Also tried setting the longLived property, but that didn't work either. I've scoured the docs and couldn't find anything that should stop the block from being called when the app is backgrounded.
I have other code that can call through their completion blocks when in the background, so not sure why this would be limited. Any ideas?

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.

GKGameSession- saveData always fails with malloc error after getShareURLWithCompletionHandler

I'm trying to incorporate GKGameSession into my Game Center game. I've tried several combinations of the following code: running the commands asynchronously, chaining them in the completion handlers, etc. Every time I see the same result: I can use saveData just fine until I've called getShareURLWithCompletionHandler. After that, any attempt to saveData throws an error.
Here's the simplest version of code that exhibits the problem:
CKContainer *defaultContainer = [CKContainer defaultContainer];
[GKGameSession createSessionInContainer:defaultContainer.containerIdentifier
withTitle:#"temp title"
maxConnectedPlayers:4
completionHandler:^(GKGameSession * _Nullable session, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
[session getShareURLWithCompletionHandler:^(NSURL * _Nullable url, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
}];
NSData *newData = [NSData dataWithBytesNoCopy:#"abcdefghijklmnopqrstuvwxyz" length:26];
[reSession saveData:newData completionHandler:^(NSData * _Nullable conflictingData, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
}];
}];
In most cases, the saveData call simply crashes:
malloc: *** error for object 0x32df14: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
But sometimes it throws an error:
GKGameSessionErrorDomain:GKGameSessionErrorUnknown
I've tried different kinds of data being saved. I've tried making the calls sequential by chaining all the calls in completion handlers. I've tried doing the URL fetch and data save inside and outside of the creationSession completion handler.
Is there something I'm doing wrong here?
I see the same, but with a more useful error:
The requested operation could not be completed because the session has been updated on the server, causing a conflict.
The save documentation says,
It is up to the developer to decide how to handle save conflicts.
Here though, retrying the save fails every time, forever. So yeah, that's the same state you're in.
However, when the player joining the game enters the URL on their device, their GKGameSessionEventListener's didAddPlayer: is called, and then if they save... they get the same conflict error, but if they then retry the save... it works!
The player creating the link is locked out of saving or updating game state, until joining players have updated the data. When the other player saves, the original player gets a call to session:player:didSave: on the GKGameSessionEventListener.
At that point the original player can then save as expected.
You should put one block inside other. Because blocks may be completed in any order.
I have working code like this:
NSData *newData = [NSData dataWithBytesNoCopy:#"abcdefghijklmnopqrstuvwxyz" length:26];
[reSession saveData:newData completionHandler:^(NSData * _Nullable conflictingData, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
} else {
[session getShareURLWithCompletionHandler:^(NSURL * _Nullable url, NSError * _Nullable error)
{
if (error)
{
[self printError:error];
}
}];
}
}];

Modify data with CloudKit

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.

Core Data: How to get a notification when document is saved?

I have noticed that when I call
[context save:nil];
the saving doesn't occur instantly. I tested that when I try to save and quit the app in one or two seconds. It only works if I keep the app open for 5+ seconds or so.
I have 2 questions:
How can I know when the save is complete? A simple NSLog() will be enough, just for testing purposes.
Can I force a save? Should I?
Testing if [context save] is synchronous
I have tested that many times and this is not the behavior that I'm getting. If I have this code:
[context save:nil]
NSLog(#"Saved");
I see the "Saved" log, quit the app, and when I launch it again and try to fetch the data - nothing there. This doesn't happen if I wait about 5-10 seconds after I see the "Saved" message.
Thoughts?
Some code
- (void)storeSales:(NSArray *)sales {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"EE LLLL d HH:mm:ss Z YYYY"];
for (NSDictionary *saleDictionary in sales) {
Sale *sale = [NSEntityDescription insertNewObjectForEntityForName:#"Sale" inManagedObjectContext:self.context];
sale.productName = [saleDictionary objectForKey:#"description"];
sale.date = [formatter dateFromString:[saleDictionary objectForKey:#"occured_at"]];
NSLog(#"Stored new sale in database.");
}
[self.context save:nil];
}
How I setup the UIManagedDocument
#property (nonatomic, strong) UIManagedDocument *document;
#property (nonatomic, strong) NSManagedObjectContext *context;
...
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"SalesBot Database"];
self.document = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[self.document.fileURL path]]) {
[self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {}];
} else if (self.document.documentState == UIDocumentStateClosed) {
[self.document openWithCompletionHandler:^(BOOL success) {}];
}
self.context = self.document.managedObjectContext;
UPDATE 1
I tried using NSNotificationCenter to receive NSManagedObjectContextDidSaveNotification - but - I'm receiving it twice! Once right after [context save:nil] and again 5-10 seconds later!
So it seems like you are using UIManagedDocument above Core Data. This explains some things.
At first, use -[UIDocument saveToURL:forSaveOperation:completionHandler:] to save your document. From docs:
You should typically use the standard UIDocument methods to save the document.
If you save the child context directly, you only commit changes to the parent context and not to the document store. If you save the parent context directly, you sidestep other important operations that the document performs.
UIManagedDocument works with two managed object contexts. One is working on main thread, the second is saving changes to file in background thread. This is why your changes were saved, but after reopening were lost. The second context did not finish save operation.
This explains also why your notification was triggered two times. One for each context.
The save method is not an asynchronous process. Anything that occurs after you call the save method will be executed after it has saved.
NSLog(#"About to force a save...");
[context save:nil];
NSLog(#"Now I know the save is complete!");
You can observe notification NSManagedObjectContextDidSaveNotification.

Core Data storage from remote data source

I am developing an iPhone app that gathers data from 3 separate feeds. In applicationDidFinishLaunching and applicationWillEnterForeground and Do the following:
[self emptySchedule];
[self populateSchedule];
[self emptyPlayers];
[self populatePlayers];
[self emptyNews];
[self populateNews];
The empty methods simply remove info from core data, and the populate methods add info back to core data by calling various web json/xml feeds. It seems to do this very fast; but was wondering if this is the preferred method for keeping information up to date in the app.
EDIT:
Just to give some context, here are a couple methods used for empty/populate:
Since this is mostly asynchronous will it affect application launch time?
- (void) emptySchedule
{
NSFetchRequest * allEvents = [[NSFetchRequest alloc] init];
[allEvents setEntity:[NSEntityDescription entityForName:#"Event" inManagedObjectContext:self.managedObjectContext]];
[allEvents setIncludesPropertyValues:NO]; //only fetch the managedObjectID
NSError * error = nil;
NSArray * events = [self.managedObjectContext executeFetchRequest:allEvents error:&error];
//error handling goes here
for (NSManagedObject * event in events) {
[self.managedObjectContext deleteObject:event];
}
NSError *saveError = nil;
[self.managedObjectContext save:&saveError];
}
-(void)populateSchedule
{
NSURL *url = [NSURL URLWithString:SCHEDULE_FEED_URL];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id schedule)
{
for (NSDictionary *campEvent in schedule)
{
Event *event = nil;
event = [NSEntityDescription insertNewObjectForEntityForName:#"Event" inManagedObjectContext:self.managedObjectContext];
event.eventName = [campEvent valueForKeyPath:#"eventName"];
event.ticketsRequired = [campEvent valueForKeyPath:#"ticketsRequired"];
event.location = [campEvent valueForKeyPath:#"location"];
event.practiceStart = [NSDate dateWithTimeIntervalSince1970:[[campEvent valueForKeyPath:#"practiceStart"] doubleValue]];
event.practiceEnd = [NSDate dateWithTimeIntervalSince1970:[[campEvent valueForKeyPath:#"practiceEnd"] doubleValue]];
}
NSError *saveError = nil;
//Save inserts
[self.managedObjectContext save:&saveError];
//Notify other objects of this
[[NSNotificationCenter defaultCenter] postNotificationName:#"populateSchedule" object:nil];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"Error" message:#"Error Retrieving Data. Please try again later." delegate:self cancelButtonTitle:#"Ok" otherButtonTitles:nil];
[alert show];
}];
[operation start];
}
I'll try to answer based on my personal experience. Maybe someone else could have a different opinion on it.
In your case the syncing is performed only in phases of the application lifecycle.
So, I would add a third. When the user asks it. But it strictly depends on the nature of your application. Another way could be to set up a background thread that periodically wakes up and asks the server to send it new data. This could be more complex than the manually syncing.
About these two ways I would perfom import operation in specific background threads. You could set up your own operations (NSOperation class is there also for this type of task) and do stuff there or use new iOS 5 Queue Core Data API.
If you haven't done already (the background importing) do it also for your actual methods (I think in this case you could just unify the pairs empty/populate). This mechanism allows you to boost application startup and say to the user what is going on without freeze the UI:"Hello, I'm fetching data from the server! Please wait".
Edit
About the code you added it's ok for me. Only two considerations.
First, if the deletion is performed in the main thread, it could block the main thread if you have a lot of entries to remove. In this case, the UI could be not responsive. Anyway you have done a good job setting setIncludesPropertyValues to NO.
About the other snippet, I guess only the data download is perfomed in an asynchronous fashion. The completion handler is performed in the main thread (you can check for example with BOOL isMainThread = [NSThread isMainThread]) and so the core data object creation and its relative saving. Also in this case if you have a lot of data the main thread could be blocked.
Anyway, if you have done some tests and the app doesn't take too long to startup, you could just remain with your code. If you start to see some sort of latency, maybe you could do Core Data operations in backgrounds.
With no iOS 5 API the save call could (I say could since you can save chuncks of data and not the whole one) take time to be performed (in particular when you have a lot of objects to store in your core data file). Starting form iOS 5 you could take advantage of new type of NSManagedObjectContext (queue concurrency type) and parent-child context. Furthermore you can avoid to write the entire Core Data stack and use the UIManagedDocument class. By means of both the save can be performed in a concurrent queue without blocking the main thread.
Hope that helps.

Resources