CloudKit helps developers manage static data and almost every App needs some static data to function properly. By static data, I mean, non mutable data. Apps that use non mutable data could be:

an App that stores information about a URL and an API key locally so that it can connect to some web service;

a reference App, such as a dictionary or a translator, that needs to quickly access the dictionary entries in order to work offline;

a quiz game, like a trivia App, that needs access to the questions and answers so that users can play offline.

Most of the time, these data need to be available at the application launch. So, we usually store this information in the App. However, we have an issue here: if we need to change these data for any reason, we will need to upload a new App version to the App Store. And you know, because we must wait a few days to pass the review , this process is not instantaneous. And it won’t be until our users get the update that everything will run smoothly again.

So, it seems that this is not the best process. In general, you don’t need to change the static data every time you send a new app version to the App Store. But there are some uncommon situations that require a solution as quickly as possible. For example, imagine that the API key of your app became invalid. Maybe someone hacked your account, stole the API keys and started to use them incorrectly, costing you thousands of dollars, and putting the privacy of your users at risk. In this case, we would need to reset the API key as soon as possible, but going through the review process can be killer.

Though this never should happen, it can happen. And while you’re trying to find out who hacked your account, time is passing. Your App is losing the momentum, or, worst case, not working at all. So, we have to fix it, and we have to do it now!

CloudKit to the rescue

CloudKit is the elegant and easy solution to these uncommon situations. And in this tutorial I will show you how to use CloudKit to effectively and intuitively deal with these development dilemmas.

What we need to do is the following. First, since we do not want any latency when the application first launches and downloads the data from the server, we can still put the static data in the main bundle of the App as before. This time, we also replicate the data in CloudKit. Then, the App checks if the data in CloudKit have been updated. In case of any change, our App will sync and get the new data locally.

Architecting the App in this way will prevent headaches in the future.

CloudKit Public Database

When using CloudKit, your App accesses data stored in an iCloud container (an instance of CKContainer ). An iOS or OS X App can access its default container or other containers that you have created and shared among your other Apps. For example, your iOS App can access a container to update its local data. You can have another App, for example a Mac App, that has access to the same container and is in charge of populating the data.

Inside each container, there are two databases: a public database and a private database. The public database can be used to store the app’s data. Every user can access the public database without needing to enter the iCloud credentials. We can use the public database in our App, even if the user does not have an iCloud account, as long as we do it only for reading data, which is what we need to implement our solution.

Each user can also access her private database. In this case, the user must have an iCloud account setup on her device. In this tutorial, we are not going to discuss how to access a private database.

Enabling CloudKit in your App

Enabling CloudKit in your App is very simple. Select the App target, and go to the Capabilities pane. Switch on the iCloud switch, and select the CloudKit checkbox. In this way, Xcode creates a default container for your App and adds the CloudKit framework to the project.

Using CloudKit to update your data

In this first example, we will solve the case of having to update a setting of the App, like the server URL and its API key.

To access the schema of the database, click on the button CloudKit Dashboard, or go https://icloud.developer.apple.com/dashboard. In the CloudKit dashboard, you can manage the schema of the database, create new Record Types, and add, edit and delete records.

Select Record Types and then click on the + button to create a new record type. Name the new record type WebServiceSettings . Then, add two fields of type string. Call them serviceURL and serviceAPIKey . By default, when you create new fields, indexes are added, to allow sorting, querying and searching on them. In this case, we can uncheck all of them as we are only going to perform direct fetches of a record based on its record name.

Now, select Default Zone , chose WebServiceSettings from the popup menu, and click on the New Record button to add a new record. In the Record Name field, type a unique identifier. We will use that identifier to fetch the record from the App. By default, the dashboard gives you one like 99cae8ed-274e-4f56-87d2-83b27180c3fb , but you can use any name record you want, as long as it is unique across this database. I am going to use awesomeWebService for this example. Now, type a URL and an API key in the record fields, and click on the Save button.

Let’s go back to Xcode. In the AppDelegate.m, add this method and then call it when your App launches:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func updateWebServiceSettings ( ) { let publicDatabase = CKContainer . defaultContainer ( ) . publicCloudDatabase let recordID = CKRecordID ( recordName : "awesomeWebService" ) publicDatabase . fetchRecordWithID ( recordID ) { ( record : CKRecord ? , error : NSError ? ) -> Void in guard error == nil else { return } // Update the local copy of the settings if let localRecord = record , let urlString = localRecord [ "serviceURL" ] as ? String , let apiKey = localRecord [ "serviceAPIKey" ] as ? String { self . updateSettingsWithServiceURL ( NSURL ( string : urlString ) ! , serviceApiKey : apiKey ) } } } func application ( application : UIApplication , didFinishLaunchingWithOptions launchOptions : [ NSObject : AnyObject ] ? ) -> Bool { updateWebServiceSettings ( ) return true }

Or in Objective-C:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 - ( void ) updateWebServiceSettings { CKDatabase *publicDatabase = [ [ CKContainer defaultContainer ] publicCloudDatabase ] ; CKRecordID *recordID = [ [ CKRecordID alloc ] initWithRecordName : @"awesomeWebService" ] ; [ publicDatabase fetchRecordWithID :recordID completionHandler : ^ ( CKRecord * _Nullable record , NSError * _Nullable error ) { if ( error ) { // Handle here the error } else { // Update the local copy of the setting [ self updateSettingsWithServiceURL :record [ @"serviceURL" ] andServiceAPIKey :record [ @"serviceAPIKey" ] ] ; } } ] ; } - ( BOOL ) application : ( UIApplication * ) application didFinishLaunchingWithOptions : ( NSDictionary * ) launchOptions { // Update web service settings [ self updateWebServiceSettings ] ; return YES ; }

As you can see in the above source code, we are just getting a reference to the public database. Then we create a recordId with the name of the record we are interested in (remember that this id is unique). And finally, we tell the public database to perform a fetch of that record. If the fetch succeeds, we get back an instance of the CKRecord class with the values of its fields. At this point, you should update your local copy of the data with the new values.

CloudKit Subscriptions

Using these few lines of code, we are now able to update our app data. If we need to change the service URL or the API key, we only need to go to the CloudKit Dashboard, select the record and modify its values. The next time the user launches the App the data will be updated.

But this is not enough. We can do it even better. We do not want to wait for the next time the user launches the App, and we do not want to be fetching the record each time if it has not changed. Instead, we want to minimize the fetches that we make to CloudKit, and only fire a fetch if the record has actually changed. So, how do we do that? The answer is using CloudKit subscriptions.

Now when the App launches, instead of directly performing a fetch to CloudKit, what we are going to do first is to check if we have already subscribed to the record changes. If we are subscribed we do not need to do anything else, as we will be notified by CloudKit via remote push notifications. If we are still not subscribed (for example, the first time the user uses the App), before subscribing, we will need to perform a fetch of the record, to be sure that we have the most updated version of the data. Then, we will check if the user has an iCloud account available. In this case, we need an iCloud account to be available, because we are going to save the subscription on CloudKit, and therefore we need write permissions. If the iCloud account is available, we can create a subscription and save it on the public database. The subscription is setup to be fired anytime the record changes.

Let’s add this new method to our AppDelegate and call it when the App launches, instead of calling the other one:

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 func subscribeToWebServiceSettingsChanges ( ) { let subscribed = NSUserDefaults . standardUserDefaults ( ) . boolForKey ( "subscribedToUpdates" ) if subscribed == false { let publicDatabase = CKContainer . defaultContainer ( ) . publicCloudDatabase let recordID = CKRecordID ( recordName : "awesomeWebService" ) publicDatabase . fetchRecordWithID ( recordID , completionHandler : { ( record : CKRecord ? , error : NSError ? ) -> Void in guard error == nil else { //Handle error here return } if let localRecord = record , let urlString = localRecord [ "serviceURL" ] as ? String , let apiKey = localRecord [ "serviceAPIKey" ] as ? String { self . updateSettingsWithServiceURL ( NSURL ( string : urlString ) ! , serviceApiKey : apiKey ) CKContainer . defaultContainer ( ) . accountStatusWithCompletionHandler ( { ( accountStatus : CKAccountStatus , error : NSError ? ) -> Void in guard error == nil else { // Handle error here return } if accountStatus == CKAccountStatus . Available { let predicate = NSPredicate ( format : "YOUR PREDICATE HERE" ) let subscription = CKSubscription ( recordType : "WebServiceSettings" , predicate : predicate , options : CKSubscriptionOptions . FiresOnRecordUpdate ) publicDatabase . saveSubscription ( subscription , completionHandler : { ( subscription : CKSubscription ? , error : NSError ? ) -> Void in guard error == nil else { // Handle error here return } NSUserDefaults . standardUserDefaults ( ) . setBool ( true , forKey : "subscribedToUpdates" ) NSUserDefaults . standardUserDefaults ( ) . synchronize ( ) } ) } } ) } } ) } }

And 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 - ( void ) subscribeToWebServiceSettingsChanges { BOOL isSubscribed = [ [ NSUserDefaults standardUserDefaults ] boolForKey : @"subscribedToUpdates" ] ; if ( isSubscribed == NO ) { // First, we update the record CKDatabase *publicDatabase = [ [ CKContainer defaultContainer ] publicCloudDatabase ] ; CKRecordID *recordID = [ [ CKRecordID alloc ] initWithRecordName : @"awesomeWebService" ] ; [ publicDatabase fetchRecordWithID :recordID completionHandler : ^ ( CKRecord * _Nullable record , NSError * _Nullable error ) { if ( error ) { // Handle here the error } else { // Update the local copy of the setting [ self updateSettingsWithServiceURL :record [ @"serviceURL" ] andServiceAPIKey :record [ @"serviceAPIKey" ] ] ; // Then, we check if there is an iCloud account available so we can have write permission [ [ CKContainer defaultContainer ] accountStatusWithCompletionHandler : ^ ( CKAccountStatus accountStatus , NSError * _Nullable error ) { if ( accountStatus == CKAccountStatusAvailable ) { // Then, subscribe to future updates NSPredicate *predicate = [ NSPredicate predicateWithFormat : @"TRUEPREDICATE" ] ; CKSubscription *subscription = [ [ CKSubscription alloc ] initWithRecordType : @"WebServiceSettings" predicate :predicate options :CKSubscriptionOptionsFiresOnRecordUpdate ] ; [ publicDatabase saveSubscription :subscription completionHandler : ^ ( CKSubscription * _Nullable subscription , NSError * _Nullable error ) { if ( error ) { // Handle here the error } else { // Save that we have subscribed successfully [ [ NSUserDefaults standardUserDefaults ] setBool :YES forKey : @"subscribedToUpdates" ] ; [ [ NSUserDefaults standardUserDefaults ] synchronize ] ; } } ] ; } } ] ; } } ] ; } }

Now, modify the application: didFinishLaunchingWithOptions: in the following way:

1 2 3 4 func application ( application : UIApplication , didFinishLaunchingWithOptions launchOptions : [ NSObject : AnyObject ] ? ) -> Bool { subscribeToWebServiceSettingsChanges < / code > < code > ( ) return true }

Or in Objective-C:

1 2 3 4 5 - ( BOOL ) application : ( UIApplication * ) application didFinishLaunchingWithOptions : ( NSDictionary * ) launchOptions { // Subscribe to web service settings changes [ self subscribeToWebServiceSettingsChanges ] ; return YES ; }

Every time the record changes in CloudKit, a remote push notification will be sent to the device, so we also have to subscribe to receive push notifications and to process them when they arrive. Add this code to the application: didFinishLaunchingWithOptions: to register for push notifications:

1 2 3 4 5 6 7 8 9 10 11 func application ( application : UIApplication , didFinishLaunchingWithOptions launchOptions : [ NSObject : AnyObject ] ? ) -> Bool { // Push notification setup let notificationSettings = UIUserNotificationSettings ( forTypes : UIUserNotificationType . Alert , categories : nil ) application . registerUserNotificationSettings ( notificationSettings ) application . registerForRemoteNotifications ( ) subscribeToWebServiceSettingsChanges ( ) return true }

Or in Objective-C:

1 2 3 4 5 6 7 8 9 10 11 - ( BOOL ) application : ( UIApplication * ) application didFinishLaunchingWithOptions : ( NSDictionary * ) launchOptions { // Push notification setup UIUserNotificationSettings *notificationSettings = [ UIUserNotificationSettings settingsForTypes :UIUserNotificationTypeAlert categories :nil ] ; [ application registerUserNotificationSettings :notificationSettings ] ; [ application registerForRemoteNotifications ] ; // Subscribe to web service settings changes [ self subscribeToWebServiceSettingsChanges ] ; return YES ; }

And finally implement this method in your AppDelegate to handle them:

1 2 3 4 5 6 7 8 9 10 func application ( application : UIApplication , didReceiveRemoteNotification userInfo : [ NSObject : AnyObject ] ) { let cloudKitNotification = CKNotification . init ( fromRemoteNotificationDictionary : userInfo as ! [ String : NSObject ] ) if cloudKitNotification . notificationType == CKNotificationType . Query { let recordID = ( cloudKitNotification as ! CKQueryNotification ) . recordID if recordID ? . recordName == "awesomeWebService" { updateWebServiceSettings ( ) } } }

Or in Objective-C:

1 2 3 4 5 6 7 8 9 10 11 - ( void ) application : ( UIApplication * ) application didReceiveRemoteNotification : ( NSDictionary * ) userInfo { CKNotification *cloudKitNotification = [ CKNotification notificationFromRemoteNotificationDictionary :userInfo ] ; if ( cloudKitNotification . notificationType == CKNotificationTypeQuery ) { CKRecordID *recordId = [ ( CKQueryNotification * ) cloudKitNotification recordID ] ; // if the notification corresponds to a change in our web service record, then we fetch the new values if ( [ recordId . recordName isEqualToString : @"awesomeWebService" ] ) { [ self updateWebServiceSettings ] ; } } }

Ok, let’s run the App. Then, go to the CloudKit dashboard and make changes to the record. You will see the the App receives the notification almost immediately and fetches the changes. If we need to make a critical update to any of the fields, now we can propagate the changes to all our users almost immediately.

Handling errors

As you can see from the previous source code examples, all calls to the CloudKit API could fail. If any operation cannot be performed, you will receive an error instead of the data. This error can have extra information in the userInfo dictionary that can help you make the correct decision to recover from it.

For example, if an operation cannot be performed because the service is unavailable, or you have exceeded the rate limits, there will be an entry in the dictionary for the key CKErrorRetryAfterKey . Its value is a NSNumber containing the number of seconds after you should retry the request:

1 2 3 4 5 6 7 8 9 10 11 publicDatabase . fetchRecordWithID ( recordID , completionHandler : { ( record : CKRecord ? , error : NSError ? ) -> Void in guard error == nil else { // Handle error here if let secondsBeforeRetrying = error ! . userInfo [ CKErrorRetryAfterKey ] as ? NSNumber { self . retryUpdatingWebServiceAfter ( secondsBeforeRetrying ) } return } ...

Or in Objective-C:

1 2 3 4 5 6 7 8 9 10 11 12 13 [ publicDatabase fetchRecordWithID :recordID completionHandler : ^ ( CKRecord * _Nullable record , NSError * _Nullable error ) { if ( error ) { // Handle here the error NSNumber *secondsToRetry = error . userInfo [ CKErrorRetryAfterKey ] ; if ( secondsToRetry ) { // Retry the operation after an amount of seconds [ self retryUpdatingWebServiceSettingsAfter :secondsToRetry ] ; } } else { // Update the local copy of the setting [ self updateSettingsWithServiceURL :record [ @"serviceURL" ] andServiceAPIKey :record [ @"serviceAPIKey" ] ] ; } } ] ;

Conclusion

CloudKit empowers your iOS apps through powerful data storage and access. In Advanced CloudKit (Part 1), we used CloudKit to change your App’s critical data. This vital functionality allows you to react quickly when problems and changes arise unexpectedly. Next week we will continue learning about CloudKit by delving into Big Data. I encourage you to continue investigating CloudKit, and using it in your Apps. To learn more, go to https://developer.apple.com/icloud where you will find lots of resources.

I hope you take advantage of CloudKit in your next Awesome App!!!

Vicente Vicens

(Visited 656 times, 1 visits today)