Advanced CloudKit (Part II): CKOperation, CKQueryOperation
Last week, we published an in-depth CloudKit tutorial on managing critical data. So, in this post, I will build on what I showed you last week by teaching you how to manage a large amount of data with CloudKit.
CloudKit and Big Data
In this second example, we are going to update many records. Let’s take a look at the case of a dictionary App. We have our local copy of the data, but when the user downloads the App from the App Store and launches it for the first time, it is possible that some of the records from the initial set of data have become outdated.
The technique we are going to use here is very similar to what we have done in the first example. But instead of fetching a single record, we are going to use a query operation to fetch a group of records at once.
This is an example of the code you would use to achieve that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
func updateWordDefinitionChanges() { // Create the initial query let predicate = NSPredicate(format: "TRUEPREDICATE") let query = CKQuery(recordType: "WordDefinitions", predicate: predicate) // Create the initial query operation let queryOperation = CKQueryOperation(query: query) let operationQueue = NSOperationQueue() self.executeQueryOperation(queryOperation, onOperationQueue: operationQueue) } func executeQueryOperation(queryOperation: CKQueryOperation, onOperationQueue operationQueue: NSOperationQueue) { let publicDatabase = CKContainer.defaultContainer().publicCloudDatabase; // Setup the query operation queryOperation.database = publicDatabase // Assign a record process handler queryOperation.recordFetchedBlock = { (record : CKRecord) -> Void in // Process each record self.updateWordDefinitionWithIdentifier(record.recordID.recordName) } // Assign a completion handler queryOperation.queryCompletionBlock = { (cursor: CKQueryCursor?, error: NSError?) -> Void in guard error==nil else { // Handle the error return } if let queryCursor = cursor { let queryCursorOperation = CKQueryOperation(cursor: queryCursor) self.executeQueryOperation(queryCursorOperation, onOperationQueue: operationQueue) } } // Add the operation to the operation queue to execute it operationQueue.addOperation(queryOperation) } |
Or in Objective-C:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
- (void)updateWordDefinitionChanges { // Create the initial query NSPredicate *predicate = [NSPredicate predicateWithFormat:@"TRUEPREDICATE"]; CKQuery *query = [[CKQuery alloc] initWithRecordType:@"WordDefinitions" predicate:predicate]; // Create the initial query operation CKQueryOperation *queryOperation = [[CKQueryOperation alloc] initWithQuery:query]; NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init]; [self executeQueryOperation:queryOperation onOperationQueue:operationQueue]; } - (void)executeQueryOperation:(CKQueryOperation *)queryOperation onOperationQueue:(NSOperationQueue *)operationQueue { CKDatabase *publicDatabase = [[CKContainer defaultContainer] publicCloudDatabase]; // Setup the query operation queryOperation.database = publicDatabase; // Assign a record process handler queryOperation.recordFetchedBlock = ^(CKRecord * record) { // Process each record [self updateWordDefinitionWithIdentifier:record.recordID.recordName]; }; // Assign a completion handler queryOperation.queryCompletionBlock = ^(CKQueryCursor * cursor, NSError * operationError) { if (operationError) { // Handle the error } else if (cursor) { CKQueryOperation *cursorQueryOperation = [[CKQueryOperation alloc] initWithCursor:cursor]; [self executeQueryOperation:cursorQueryOperation onOperationQueue:operationQueue]; } }; // Add the operation to the operation queue to execute it [operationQueue addOperation:queryOperation]; } |
In the first method, we create a query with a predicate to fetch the records. We create a CKQueryOperation
and pass it to the second method, along with the operation queue that we will use to execute it.
In the second method we setup the query:
- we assign the database where it will be executed;
- we set a block that will be executed for each record found, where we will process the changes and update the local copy of the data;
- we set a completion handler that will be executed when the operation finishes;
- we add the operation to the operation queue to execute it.
In the queryCompletionBlock
, as you can see in the source code, you should handle any errors that may occur (see our previous post on CloudKit). Note that we are checking if a CKQueryCursor
is provided. If it exists, that indicates that there are more results to fetch (because the operations are limited in size and number of records). If so, we use the cursor to initialize a new query operation and we use the same method to setup and execute it. This ensures that we will create and execute serialized query operations until we finally process all the records.
Pre-populate a CloudKit database
To populate our database of word definition changes, we are going to create a record for each of the word definitions that have changed and save it in iCloud using CloudKit. This time we are talking about lots of records, so doing it manually on the CloudKit dashboard is not the best solution. So, we are going to create another App, a Mac App, to do the work automatically.
In your project, create a new target and choose a Mac App. We need to enable CloudKit, as we did with our iOS App. Select the target that you have just created and follow the same steps, going to the Capabilities pane and switching on iCloud, and activating the CloudKit checkbox. This time we want to have access to the iCloud container of the iOS App, because we need to save the data in that container, so the iOS App can read the data from it. To do that, select Specify custom containers
. A list of available containers will appear. Look for the container of your iOS App and select its checkbox.
Now we are ready to create a CKRecord for each of our word definitions. Instead of saving them one by one, we will create a CKModifyRecordsOperation
and will save all of them at once. Use the following code as an example of how you can do that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
func uploadWordDefinitionChangesToCloudKit() { // Get a reference to the public database of the shared container let publicDatabase = CKContainer(identifier: "iCloud.com.yourdomain.YourAwesomeApp").publicCloudDatabase // Create an array of CKRecord instances to upload var recordsToUpload = [CKRecord]() for wordDefinition in self.wordDefinitionsToUpload() { let recordId = CKRecordID(recordName: wordDefinition.identifier) let record = CKRecord(recordType: "WordDefinitions", recordID: recordId) record["word"] = wordDefinition.word record["definition"] = wordDefinition.definition recordsToUpload.append(record) } // Create a CKModifyRecordsOperation operation let uploadOperation = CKModifyRecordsOperation(recordsToSave: recordsToUpload, recordIDsToDelete: nil) uploadOperation.atomic = false uploadOperation.database = publicDatabase // Assign a completion handler uploadOperation.modifyRecordsCompletionBlock = { (savedRecords: [CKRecord]?, deletedRecords: [CKRecordID]?, operationError: NSError?) -> Void in guard operationError==nil else { // Handle the error return } if let records = savedRecords { for record in records { // Mark the word definition as uploaded so is not included in the next batch self.markWordDefinitionAsUploaded(record.recordID.recordName) } } } // Add the operation to an operation queue to execute it NSOperationQueue().addOperation(uploadOperation) } |
Or in Objective-C:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
- (void)uploadWordDefinitionChangesToCloudKit { // Get a reference to the public database of the shared container CKDatabase *publicDatabase = [[CKContainer containerWithIdentifier:@"iCloud.com.yourdomain.YourAwesomeApp"] publicCloudDatabase]; // Create an array on CKRecord instances to upload NSMutableArray *recordsToUpload = [[NSMutableArray alloc] init]; for (WordDefinition *wordDefinition in [self wordDefinitionsToUpload]) { CKRecordID *recordId = [[CKRecordID alloc] initWithRecordName:wordDefinition.identifier]; CKRecord *record = [[CKRecord alloc] initWithRecordType:@"WordDefinitions" recordID:recordId]; record[@"word"] = wordDefinition.word; record[@"definition"] = wordDefinition.definition; [recordsToUpload addObject:record]; } // Create a CKModifyRecordsOperation operation CKModifyRecordsOperation *uploadOperation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:recordsToUpload recordIDsToDelete:nil]; uploadOperation.atomic = NO; uploadOperation.database = publicDatabase; // Assign a completion handler uploadOperation.modifyRecordsCompletionBlock = ^(NSArray<CKRecord *> * savedRecords, NSArray<CKRecordID *> * deletedRecordIDs, NSError * operationError) { if (operationError) { // Handle the error } else { for (CKRecord *record in savedRecords) { // Mark the word definition as uploaded so is not included in the next batch [self markWordDefinitionAsUploaded:record.recordID.recordName]; } } }; // Add the operation to an operation queue to execute it NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init]; [operationQueue addOperation:uploadOperation]; } |
The first thing to note in the code is that this time, instead of using the default container, we are using a custom container, which points to the container that the iOS App will use later to read and update the records.
Then we create an instance of a CKRecord
for each of our word definitions, and put them in an array. We create a CKModifyRecordsOperation
and pass the array of records to save to it, and also we tell it to use the public database of the custom container. We also define a completion block that will be executed when the operation finishes. To execute the operation, we create a NSOperationQueue
and add the operation to it.
In the completion handler, you should handle any error that can occur, because the operation can succeed, fail, or only save some of the records, but not all of them. There are limits to the number of records and size of the operations. For example, at this time, you can only save a maximum of 400 objects in one operation. If you try to save more objects the operation will fail with an error. If you need to save more objects, do it in batches, and control which objects have already been saved in the completion handler. In our code, we have set a completion handler that will be executed once, when the operation finishes. If you need more control over each of the records, you can also set a completion handler perRecordCompletionBlock
that will execute once per each of the records.
Once you execute this code, you will see that, in the CloudKit Dashboard, a new record type has appeared. This is possible because we are still in the Development
environment, and that makes it easy to create the database schema by code. Once you change to the Production
environment, this will not be possible, and the record types and its fields will need to exist before you make operations against them, or they will fail.
Conclusion
Thanks for reading our in-depth CloudKit tutorials. CloudKit is a powerful technology that empowers iOS apps through data storage and access. I encourage you to continue investigating CloudKit, and using it to build powerful Apps for Apple devices. To learn more, go to developer.apple.com/icloud where you will find lots of excellent resources.
Keep Coding!!!
Vicente Vicens
Hey! Thanks for this! It's exactly what I need to do for my app. I'm switching from Parse to cloudkit and need to be able to batch process about 330 items at once (a price list). This will do it. Cheers!
Thanks for reading our blog. We are glad it us useful for your work.
Any idea what would cause "Server Rejected Request - Internal server error" error?
If your code was working before, and you are receiving this error now, probably is due to a problem with the iCloud services. Today, a problem has been reported some hours ago, that affected some users. You can find info about the incidence at http://www.apple.com/support/systemstatus/
More info about the code errors can be found in the official documentation at https://developer.apple.com/library/ios/documentation/CloudKit/Reference/CloudKit_constants/index.html