CloudKit Tutorial: Getting Started

Datetime:2016-08-22 23:24:54          Topic: IOS Development           Share

Learn how to store your app’s data in the cloud with CloudKit!

Update note: This tutorial has been updated to Xcode 7.3, iOS 9.3, and Swift 2.2. The original tutorial was written by Michael Katz.

CloudKit is Apple’s remote data storage service based on iCloud. It provides a low-cost option to store and share app data using your users’ iCloud accounts as a back-end storage service.

There are two main components to CloudKit:

  1. A web dashboard to manage record types and any public data.
  2. A set of APIs to transfer data between iCloud and the device.

CloudKit is secure as well. Users’ private data is completely protected, as developers can only access their own private database and aren’t able to look at any users’ private data.

CloudKit is a good option for iOS-only applications that use a lot of data, but don’t require a great deal of server-side logic.

In this CloudKit tutorial, you’ll get hands-on experience using CloudKit by creating a restaurant rating app with a twist called, BabiFüd .

Note: To work with the sample app in this CloudKit tutorial you’ll need an active iOS developer account. Without one, you won’t be able to enable the iCloud entitlements or access the CloudKit dashboard.

Why CloudKit?

You might wonder why you should choose CloudKit over Core Data, other commercial BaaS (Back end as a Service) offerings, or even rolling your own server.

There are three reasons: simplicity, trust, and cost.

Simplicity

Unlike other backend solutions, CloudKit requires little setup. You don’t have to choose, configure, or install servers. Security and scaling are handled by Apple as well.

Simply registering for the iOS Developer Program makes you eligible to use CloudKit. You don’t have to register for additional services or create new accounts. When you enable CloudKit capabilities in your app all necessary server setup magic happens automatically.

There’s no need to download additional libraries and configure them. CloudKit is imported like any other iOS framework. The CloudKit framework itself also provides a level of simplicity by offering convenient APIs for common operations.

It’s also easy for users. Since CloudKit uses the iCloud credentials entered when the device is set up (or entered after set up via the Settings app), there’s no need to build complicated login screens. As long as they are logged in, users can seamlessly start using your app. That should put you on Cloud 9!

Trust

Another benefit of CloudKit is that users can trust the privacy and security of their data by relying on Apple rather than app developers. CloudKit insulates user data from you, the developer.

While this lack of access can be frustrating while debugging, it is a net plus since you don’t have to worry about security or convince users their data is secure. If an app user trusts iCloud, then they can also trust you.

Cost

Finally, for any developer, the cost of running a service is a huge deal. Even the least expensive server hosts cannot offer low-cost solutions for small, free or cheap apps. So there will always be a cost associated with running an app.

With CloudKit, you get a reasonable amount of storage and data transfer for public data for free. There is a very thorough explanation of fees in the What’s New in CloudKit WWDC 2015 Video.

These strengths make the CloudKit service a low-hassle solution for Mac and iOS apps.

Introducing BabiFüd

The sample app for this tutorial, BabiFüd , is the freshest take on the standard “rate a restaurant” style of app. Instead of reviewing restaurants based upon food quality, speed of service, or price, users rate child-friendliness. This includes availability of changing facilities, booster seats, and healthy food options.

The app contains four tabs: a list of nearby restaurants, a map of nearby restaurants, user-generated notes, and settings. The list of nearby restaurants is the only tab you’ll use in this tutorial. You can get a glimpse of the app in action below.

A model class backs these views and wraps the calls to CloudKit. CloudKit objects are called records . The main record type in your model is an Establishment , which represents the various restaurants in your app.

Getting Started

Start by downloading thestarter project for this tutorial.

You’ll have to change the Bundle Identifier and Team of your app before you can start coding. You need to set the team in order to get the necessary entitlements from Apple. Having a unique bundle identifier makes the process a whole lot easier.

Open BabiFud.xcodeproj in Xcode. Select the BabiFud project in the Project Navigator , then select the BabiFud target . With the General tab selected, replace the Bundle Identifier with something unique. Standard practice is to use reverse domain name notation and include the project name. Next, select the appropriate Team :

That takes care of the Bundle Identifier and Team. Now you’ll need to get your app set up for CloudKit and create some containers to hold your data.

Entitlements and Containers

You’ll need a container to hold the app’s records before you can add any data via your app. A container is the term for the conceptual location of all the app’s data on the server. It is the grouping of public and private databases.

To create a container, you first need to enable the iCloud entitlements for your app. Select the Capabilities tab in the target editor. Next flip the switch in the iCloud section to ON .

At this point, Xcode might prompt you to enter the Apple ID associated with your iOS developer account. If so, then type it in as requested. Finally, enable CloudKit by checking the CloudKit checkbox in the Services group.

This creates a default container named iCloud.<your app’s bundle id> , as illustrated below:

Troubleshooting iCloud Setup in Xcode

If you see any warnings or errors when creating entitlements, building the project, or running the app, and Xcode is complaining about the container ID, then here are some troubleshooting tips:

  • If there are any warnings or errors shown in the Steps group in the iCloud section, then try pressing the Fix Issue button. This might need to be done a few times.
  • It’s important that the app’s bundle id and iCloud containers match and exist in the developer account. For example, if the bundle identifier is “com.<your domain>.BabiFud”, then the iCloud container name should be “iCloud.” plus the bundle bundle id: “iCloud.com.<your domain>.BabiFud”.
  • The iCloud container name must be unique because this is the global identifier used by CloudKit to access the data. Since the iCloud container name contains the bundle id, the bundle id must also be unique (which is why it has to be changed from com.raywendrelich.BabiFud).
  • In order for the entitlements piece to work, the app/bundle id has to be listed in the App IDs portion of the Certificates, Identifiers, and Profiles portal. This means the certificate used to sign the app has to be from the set team id and has to list the app id, which also implies the iCloud container id.Normally, Xcode does all of this automatically if you are signed in to a valid developer account. Unfortunately, this sometimes gets out of sync. It can help to start with a fresh ID and, using the iCloud capabilities pane, change the CloudKit container ID to match. Otherwise, to fix it you may have to edit the info.plist or BabiFud.entitlements files to make sure the id values there reflect what you set for the bundle id.

Introducing the CloudKit Dashboard

After setting up the necessary entitlements, the next step is to create some record types that define the data used by your app. You can do this using the CloudKit dashboard. Click CloudKit Dashboard , found in the target’s Capabilities pane, under iCloud .

Note: You can also launch the CloudKit dashboard by opening the URL https://icloud.developer.apple.com/dashboard/ in your browser.

Here’s what the dashboard looks like:

The CloudKit dashboard consists of four sections: Schema, Public Data, Private Data, and Admin.

The SCHEMA section represents the high level objects of a CloudKit container: Record Types , Security Roles , and Subscription Types . You’ll only be concerned with Record Types in this tutorial.

A Record Type is a set of fields that defines individual records. In terms of object-oriented programming, a Record Type is like a class. A record can be considered an instance of a particular Record Type. It represents structured data in the container, much like a typical row in a database, and encapsulates a series of key/value pairs.

The PUBLIC DATA and PRIVATE DATA sections let you add data to, or search for data in the databases to which you have access. Remember, as a developer you access all public data, but only your own private data. The User Records store data about the current iCloud user such as name and email. A Record Zone (here noted as the Default Zone ) is used to provide a logical organization to a private database by grouping records together. Custom zones support atomic transactions by allowing multiple records to be saved at the same time before processing other operations. Custom zones are outside the scope of this tutorial.

The ADMIN section provides the ability to configure the dashboard permissions for your team members. If you have multiple development team members, you can restrict their ability to edit data here. This, too, is out-of-scope for this tutorial.

Adding the Establishment Record Type

Think about the design of your app for a moment. Each establishment you’ll want to track has lots of data: name, location, and availability of various child-friendly options. Record types use fields to define the various pieces of data contained in each record.

With Record Types selected, click the + icon in the top left of the detail pane to add a new record type.

Name your new record type Establishment .

You’ll see a row of fields where you can define the Field Name , Field Type , and Index , as shown below. A field with the default name of, StringField , has been automatically created for you.

Start by replacing StringField with Name . The Field Type and Index defaults already match what you need for this first field definition, but you’ll need to make changes to the Field Type and Index for some of the other fields. Click Add Field… to add new fields as required. Add the following fields:

When you’re done, your list of fields should look like this:

Click Save at the bottom of the page to save your new record type.

You’re now ready to add some sample establishment records to your database.

Select Default Zone under the PUBLIC DATA section in the navigation pane on the left. This zone will contain the public records for your app. Select the Establishment record type from the dropdown list in the center pane if it’s not already selected. Then click the + icon or the New Record button in the right detail pane, as shown in the screenshot below:

This will create a new, empty Establishment record.

At this point you’re ready to enter some test data for your app.

The following sample establishment data is fictional. The establishments are located near Apple’s headquarters so they’re easy to find in the simulator.

Enter each record as described below:

Note: The image for each CoverPhoto element is included in the Supporting Files\Sample Images folder in the Xcode project. To add the image to the establishment record, simply drag it to the CoverPhoto field.

Once all three records have been saved, the dashboard should look like this:

For each record, the values entered are the database representation of the data. On the app side, the data types are different. For example, SeatingType and ChangingTable are structs. So the specified Int value for a SeatingType might correspond to a “high chair” or a “booster” seat. For HealthyOption and KidsMenu, the Int values represent Boolean types: a 0 means that establishment doesn’t have that option and a 1 means that it does.

Running the app requires you to have an iCloud account that can be used for development.

Creating an iCloud Account for Development.

You will also need to enter into the iOS Simulator the iCloud credentials associated with this account.

Enter iCloud Credentials Before Running Your App

Return to Xcode. It’s time to start integrating this data into your app!

Querying Establishment Records

CKQuery objects are used to select records from a database. A CKQuery describes how to find all records of a specified record type that match certain criteria. These criteria can be something like “all records with a Name field that starts with ‘M’”, or “all records that have booster seats”, or “all records within 3km.” These types of expressions are coded in Cocoa with NSPredicate objects. An NSPredicate evaluates objects to see if they match the specified criteria. Predicates are also used in Core Data and are a natural fit for CloudKit, because predicates commonly are defined as a comparison on a field.

CloudKit supports only a subset of available NSPredicate functions. These include mathematical comparisons, some string and set operations (such as “field matches one of the items in a list”), and a special distance function. The function distanceToLocation:fromLocation: was added to NSPredicate for CloudKit to match records with a location field within a specified radius from a known location. This type of predicate is covered in detail below. For other types of queries, the CKQuery Class Reference contains a detailed list of the supported functions and descriptions of how to use them.

Note: CloudKit includes support for CLLocation objects. These are Core Location Framework objects that contain geospatial coordinates. This makes it quite easy to create a query for finding establishments inside of a geographic region – without doing all of the messy coordinate math yourself.

So, in Xcode, open Model/Model.swift . This file contains stubs for all the server calls your app will make.

Replace the fetchEstablishments(_:radiusInMeters:) method with the following:

func fetchEstablishments(location:CLLocation, radiusInMeters:CLLocationDistance) {
  // 1
  let radiusInKilometers = radiusInMeters / 1000.0    
  // 2
  let locationPredicate = NSPredicate(format: "distanceToLocation:fromLocation:(%K,%@) < %f", "Location", location, radiusInKilometers)    
  // 3
  let query = CKQuery(recordType: EstablishmentType, predicate: locationPredicate)    
  // 4
  publicDB.performQuery(query, inZoneWithID: nil) { [unowned self] results, error in
    if let error = error {
      dispatch_async(dispatch_get_main_queue()) {
        self.delegate?.errorUpdating(error)
        print("Cloud Query Error - Fetch Establishments: \(error)")
      }
      return
    }
 
    self.items.removeAll(keepCapacity: true)
    results?.forEach({ (record: CKRecord) in
      self.items.append(Establishment(record: record,
        database: self.publicDB))
    })
 
    dispatch_async(dispatch_get_main_queue()) {
      self.delegate?.modelUpdated()
    }
  }
}

Taking each numbered comment in turn:

  1. CloudKit uses kilometers in its distance predicates. This line simply converts radiusInMeters to kilometers.
  2. The predicate filters establishments based on their distance in kilometers from the current location. This statement finds all establishments with a location value within the specified distance from the user’s current location.
  3. CKQuery objects are created using a predicate and a record type. Both will be used when performing the query.
  4. Finally , performQuery(_:inZoneWithID:completionHandler:) sends your query up to iCloud, and waits for any matching results. By passing nil as the inZoneWithID parameter, you’re running the query against your default zone; that is, your public database. If you want to retrieve records from both public and private databases, then you have to query each database using a separate call.

Oh. That reminds me. What did CKQuery say to iCloud?

Solution Inside: Caution When Entering Select Show

After making a miraculous recovery from such a bad joke, you’re probably wondering where the CKDatabase instance, publicDB , comes from. Take a look the top of Model.swift .

let container: CKContainer
let publicDB: CKDatabase
let privateDB: CKDatabase
 
init() {
  // 1
  container = CKContainer.defaultContainer() 
  // 2
  publicDB = container.publicCloudDatabase 
  // 3
  privateDB = container.privateCloudDatabase 
}

Here you define your databases:

  1. The default container represents the one you specified in the iCloud capabilities pane.
  2. The public database is the one shared amongst all users of your app.
  3. The private database contains only the data belonging to the currently logged-in user; in this case, you.

This code will retrieve some local establishments from the public database, but it has to be wired up to a view controller in order to see anything in the app.

Setting Up the Requisite Callbacks

You can take care of notifications with the familiar delegate pattern. Here’s the protocol from the top of Model.swift that you’ll implement in your view controller:

protocol ModelDelegate {
  func errorUpdating(error: NSError)
  func modelUpdated()
}

Open MasterViewController.swift and replace the modelUpdated() method with the following:

func modelUpdated() {
  refreshControl?.endRefreshing() 
  tableView.reloadData() 
}

This is called when new data is available. All the wiring up of the table view cells to CloudKit objects has already been taken care of in tableView(_:cellForRowAtIndexPath:) . Feel free to take a look.

Next, in MasterViewController.swift , replace the errorUpdating(_:) method with the following:

func errorUpdating(error: NSError) {
    let alertController = UIAlertController(title: nil,
                                            message: error.localizedDescription,
                                            preferredStyle: .Alert)
 
    alertController.addAction(UIAlertAction(title: "Dismiss",
                                            style: .Default,
                                            handler: nil))
 
    presentViewController(alertController, animated: true, completion: nil)
}

This method is called when the query produces an error. Errors can occur as a result of poor network conditions or CloudKit-specific issues (such as missing or incorrect user credentials or no records being returned from the query).

Good error handling is essential when dealing with any kind of remote server. For now, this code just displays to the user the message returned with the error.

One very common issue, however, is the user not being logged into iCloud and/or not having the iCloud drive turned on for this app. Can you modify errorUpdating(_:) to at least handle these situations? Hint: Both errors return a CKErrorCode of 1.

Solution Inside Select Show

Build and run. You should see a list of nearby establishments.

Troubleshooting Queries

If you’re using the iOS simulator and the list doesn’t populate, then make sure you have the correct location set by selecting from the Xcode menu, Debug\Simulate Location\San Francisco, CA, USA . If you needed to change the location in Xcode, then pull the table down in the app to force a refresh instead of waiting for a location trigger.

If you’re using an iPhone or iPad, location services are enabled, and the list still isn’t populating, then the establishments aren’t close enough to your current location. You have two options: change the coordinates of the sample data to be closer to your current location or use the simulator to run the app. There is a third option, but it’s not terribly practical as you’d have to travel to Cupertino and hang around the Apple campus.

If the data isn’t appearing properly – or isn’t appearing at all – inspect the sample data using the CloudKit dashboard. Make sure all of the records are present, you’ve added them to the default zone and they have the correct values. If you need to re-enter the data, then you can delete records by clicking the trash icon .

Debugging CloudKit errors can be tricky at times. As of this writing, CloudKit error messages don’t contain a tremendous amount of information. To determine the cause of the error you’ll need to look at the error code in conjunction with the particular database operation you’re attempting. Using the numerical error code, look up the matching CKErrorCode enum. The name and description in the documentation will help narrow down the cause of the issue. See below for some examples.

Note: For a list of error codes that can be returned by CloudKit, read the CloudKit Framework Constants Reference .

Here are some common error enums and related descriptions:

  • .BadContainer – The specified container is unknown or unauthorized.
  • .NotAuthenticated – The current user is not authenticated and no user record was available. This might happen if the user is not logged into iCloud.
  • .UnknownItem – The specified record does not exist.

When you fetched the list of establishments, you probably noticed that you can see the establishment name and the services the establishments offer. But none of the images are being displayed! Are the clouds in the way?

When you retrieved the establishment records, you automatically retrieved the images as well. You still, however, need to perform the necessary steps to load the images into your app. That’ll chase those clouds away! :]

Working with Binary Assets

An asset is binary data, such as an image, that you associate with a record. In your case, your app’s assets are the establishment photos shown in the MasterViewController table view.

In this section you’ll add the logic to load the assets that were downloaded when you retrieved the establishment records.

Open Model/Establishment.swift and replace the loadCoverPhoto(_:) method with the following code:

func loadCoverPhoto(completion:(photo: UIImage!) -&gt; ()) {
  // 1
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
    var image: UIImage!
 
    // 4
    defer {
      completion(photo: image)
    }
 
    // 2
    guard let asset = self.record["CoverPhoto"] as? CKAsset,
      path = asset.fileURL.path,
      imageData = NSData(contentsOfFile: path) else {
        return
    }
 
    // 3
    image = UIImage(data: imageData)
  }
}

This method loads the image from the asset attribute as follows:

  1. Although you download the asset at the same time you retrieve the rest of the record, you want to load the image asynchronously. So wrap everything in a dispatch_async block.
  2. Assets are stored in CKRecord as instances of CKAsset , so cast appropriately. Next load the image data from the local file URL provided by the asset.
  3. Use the image data to create an instance of UIImage .
  4. Execute the completion callback with the retrieved image. Note that this defer block gets executed regardless of which return is executed. For example, if there is no image asset, then image never gets set upon the return and no image appears for the restaurant.

Build and run. You’ve chased the clouds away and the establishment images should now appear. Great job!

There are two gotchas with CloudKit assets:

  1. Assets can only exist in CloudKit as attributes on records. You can’t store them on their own. Deleting a record will also delete any associated assets.
  2. Retrieving assets can negatively impact performance because the assets are downloaded at the same time as the rest of the record data. If your app makes heavy use of assets, then you should store a reference to a different type of record that holds just the asset.

Where To Go From Here?

You can see what’s been built so far in theFinal Project. The app is now able to download Establishment records and load their details and photos into the table view.

You can enhance the app in several ways:

  • Allow the user to add their own pictures, notes, reviews, and complaints. This will prevent them from re-visiting a bad experience.
  • Let the user create new Establishment records using the map. The functions added to your Model class can serve as an example for saving the record to either the public or private databases.
  • Add filtering and searching. The Model class constructs a CKQuery with a distance predicate, but this can be modified to be a more complex predicate. CloudKit supports text searching of string fields as well.
  • Improve the performance of the app and the data loading experience. This tutorial used the available convenience methods to invoke the necessary completion handlers when everything is ready. Instances of CKDatabase also have NSOperation -based methods that provide more control over how the API is executed.
  • Provide caching and synchronization so the app remains responsive offline and keeps the content up to date when it reconnects to a network.

You can also check out Brian Moakley’s Video Tutorial Series on CloudKit .

If you have any questions or comments about this tutorial, then please join the forum discussion below!

With CloudKit you can really take your apps to the next level and beyond with this great Apple-provided backend API. We can’t wait to see what you make. In the meantime, feel free to join us and our kids for a slice at Caesar’s Pizza!





About List