CloudKit, Apple’s new remote data storage service for apps based on iCloud, provides a low-cost option to store and share app data using users’ iCloud accounts as a back-end storage service.
There are two parts to CloudKit:
- A web dashboard to manage the record types along with any public data.
- A set of APIs to transfer data between iCloud and the device.
Cloud Kit 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?
First things first, 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.
The answers are three: simplicity, trust, and cost.
Simplicity
Unlike other backend solutions, CloudKit requires little setup. You don’t have to choose, configure, or install servers and worry about scaling and security.
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. As part of turning on the CloudKit capabilities, all the necessary setup magic on the servers 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 convenience APIs for common operations.
There’s also simplicity for users. Since CloudKit uses the iCloud credentials already entered when the device is set up (or through the Settings app), there’s no need for building complicated login screens. As long as they are logged in, users can seamlessly start using your app!
Trust
Another benefit to CloudKit is that users can trust the privacy and security of their data by trusting Apple, rather than app developers, since CloudKit insulates the users’ data from you.
While this lack of access can be frustrating during development (for debugging) it is a net plus since you don’t have to worry about security or convince users their data is secure. Even if an app user isn’t aware of CloudKit’s nuances, if they trust iCloud they can trust you.
Cost
Finally, for any developer the cost of running a service is a huge deal. Even the cheapest server hosts do not care if your app is free or cheap, and so it will cost even a little bit to run an app.
With CloudKit, you get a reasonable amount of storage and data transfer for public data for free. Seehttps://developer.apple.com/icloud/documentation/cloudkit-storage/ for details.
These strengths make the CloudKit service a low-hassle no-brainer for Mac and iOS apps.
Introducing BabiFüd
The sample app for this CloudKit tutorial, BabiFüd, is the freshest take on the standard “rate a location” style of app. However, instead of reviewing locations based upon food quality, speed of service, or price, users rate locations in terms of child-friendliness. This includes availability of changing facilities, booster seats, and healthy food options.
The app contains four tabs: a list of nearby locations, a map showing nearby locations, user-generated notes, and settings. You can get a glimpse of the app in action from the two screenshots below:
A model class backs these views and wraps the calls to CloudKit. CloudKit objects are termed records; the main record type in your model is an
Establishment
, which represents the various locations in your app. Along the way you’ll add Rating
and Note
records to your database as well.Getting Started
Start by downloading the starter project for this CloudKit tutorial.
You’ll have to change the Bundle Identifier and Team of your app before you can start coding. You need a team set in order to get the necessary entitlements from Apple, and having a unique bundle identifier makes the process a whole lot easier.
To do this, open BabiFud.xcodeproj in Xcode. Select the BabiFud project in the Project Navigator, and then select the BabiFud target. With the General tab selected, replace the Bundle Identifier with something unique; I recommend using reverse domain name notation, and include the project name. Next, select the appropriateTeam:
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 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.
To do so, select the Capabilities tab in the target editor, and then flip the switch in the iCloud section to ON, as shown in the screenshot below:
At this point, Xcode might prompt you to enter the Apple ID associated with your iOS developer account; if so, 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 in the screenshot below:
If you see any warnings or errors when creating entitlements, when building the project, or when running the app, and it’s complaining about the container ID, here are some troubleshooting tips:
- If there are any warnings or errors shown in Steps group in the iCloud section, try pressing the Fix Issuebutton. 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, this means that 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 Certificates, Identifiers, and Profiles portal. This means that that 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 for you, if you are signed in to a valid developer account. Unfortunately sometimes this gets out of sync. It can help to start with a fresh ID, and then change the CloudKit container ID to match, using the iCloud capabilities pane. Otherwise, to fix it you may have to edit the info.plist or in 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 when implementing CloudKit is to create some record types that define the data used by your app, and you can do this using the CloudKit dashboard. Click CloudKit Dashboard, found in the target’s Capabilities pane, under iCloud, as shown below:
Note: You can also launch the CloudKit dashboard by opening the URLhttps://icloud.developer.apple.com/dashboard/ in your browser.
You’ll see the dashboard appear, like so:
Here’s a basic overview of what you’ll see in the CloudKit dashboard, for the purposes of this tutorial:
The SCHEMA section of the left-hand pane 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 attributes that define individual records. In terms of object orientated programming, a Record Type is like a class template for individual objects. 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 you have access to; 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 theDefault 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 book.
The ADMIN section provides a means to configure the dashboard permissions available to your team members. If you have multiple people on your development team, you can restrict their ability to edit data here. This too is out-of-scope for this book.
Adding the Establishment Record Type
With Record Types selected, click the + icon in the top left of the detail pane to add a new record type, as shown below:
Name your new record type Establishment.
Think about the design of your app for a moment. Each establishment you’ll want to track has lots of data: a name, a location, and whether or not various child-friendly options are available. Record types use attributes to define the various pieces of data contained in each record.
You’ll see a row of fields where you can define the Name, Attribute Type, and Index, as shown below. A template StringAttribute has been automatically created for you.
Replace StringAttribute and then add the following attributes one at a time. Click Add Attribute… to add new attributes as required:
When you’re done, your list of attributes should look like the following:
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 in the right detail pane, as shown in the screenshot below:
This will create a new, empty Establishment record as shown below:
At this point you’re ready to enter some test data for your app.
The following sample establishment data is fictional; the data has them 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. The image uploads automatically when the record is saved.
Once all three records have been saved, the dashboard should look like:
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 an enum, so the specified Int value corresponds to the of the enumeration values. 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.
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 attribute 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. A NSPredicate
evaluates objects to see if they match the specified criteria. Predicates are also used a lot in Core Data and are a natural fit for CloudKit, because predicates commonly are defined as a comparison on an attribute.
CloudKit supports only a subset of available
NSPredicate
functions. These include mathematical comparisons, some string and set operations (such as “attribute matches one of the items in a list”), and new special distance function. The function distanceToLocation:FromLocation:
was added to NSPredicate
for CloudKit to match records with a location field within a specified radius for a known location. This type of predicate is covered in detail below. For other types of queries, the CKQuery class reference has a detailed list of the supported functions and 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 run a query for finding establishments inside of a geographic region – without doing all of the messy coordinate math yourself.
Open Model\Model.swift; this contains stubs for all server calls.
Replace
fetchEstablishments(location:, radiusInMeters:)
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) { results, error in if error != nil { dispatch_async(dispatch_get_main_queue()) { self.delegate?.errorUpdating(error) return } } else { self.items.removeAll(keepCapacity: true) for record in results{ let establishment = Establishment(record: record as CKRecord, database: self.publicDB) self.items.append(establishment) } dispatch_async(dispatch_get_main_queue()) { self.delegate?.modelUpdated() return } } } } |
Taking each numbered comment in turn:
- CloudKit uses kilometers in its distance predicates; this line simply converts
radiusInMeters
to kilometers. - The predicate filters establishments based on their distance in kilometers from the current location. This statement finds all establishments with a location value that puts them within the specified distance from the user’s current location.
- CKQuery objects are created using a predicate and a record type. Both will be used when performing the query.
- Finally,
performQuery(_:, inZoneWithID:, completionHandler:)
sends your query up to iCloud, returning any matching results. In this case, you’re running the query against your default zone; that is, your public database. This is because you passednil
as theinZoneWithID
parameter. If you want to retrieve records from both public and private databases, you have to query each database using a separate call.
This is all fine and good, but you’re probably wondering where the
CKDatabase
instance publicDB
comes from. Take a look the top of Model
: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:
- The default container represents the one you specified in the iCloud capabilities pane.
- The public database is the one shared among all users of your app.
- 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 ofModel.swift that you’ll implement in your view controller:
protocol ModelDelegate { func errorUpdating(error: NSError) func modelUpdated() } |
Open MasterViewController.swift and replace
modelUpdated()
with the following:func modelUpdated() { refreshControl?.endRefreshing() tableView.reloadData() } |
This executes 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.
Now replace
errorUpdating(error:)
with the following:func errorUpdating(error: NSError) { let message = error.localizedDescription let alert = UIAlertView(title: "Error Loading Establishments", message: message, delegate: nil, cancelButtonTitle: "OK") alert.show() } |
Any errors produced by query result in this method being called. Errors can occur from poor networking conditions, or CloudKit specific issues such as missing or incorrect user credentials, or when the record you requested wasn’t found.
Note: Good error handling is essential when dealing with any kind of remote server. For now, you’ll just display a popup informing the user that there has been an issue.
Build and run using the simulator. You should see list of nearby establishments, as shown below:
You can see the establishment name, and the services the establishments offer, but none of the images are being displayed. What gives?
When you retrieved the establishment records, you automatically retrieved the images as well. However, you still need to perform the necessary steps to load the images into your app.
Troubleshooting Steps
If the list doesn’t populate, make sure you have the correct location set by selecting Debug\Location\Apple. If you needed to change the location, pull the table down 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, it’ll be because the establishments aren’t close enough in to your current location. You have two options here: either 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, and that you’ve added them to the default zone and they have the correct values. If you need to re-enter the data, you can delete records by clicking the trash icon as shown below:
Debugging CloudKit errors can be tricky at times. As of writing, CloudKit error messages don’t contain a tremendous amount of information, so to determine the cause of the error you’ll need to look at the error code and what 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 good primer on CloudKit errors, read the official Apple documentation on the CKErrorCode enum under CloudKit constants.
Here are some common error enums, and suggestions on how to address them:
.BadContainer
and.MissingEntitlement
- Check that you have specified a container in the iCloud entitlements section, that it matches the CKContainer object, and that the container exists in the CloudKit dashboard.
.NotAuthenticated
and.PermissionFailure
- Check that you’ve correctly entered your iCloud credentials in Settings.app and that iCloud is enabled.
.UnknownItem
- Check that your RecordType string matches the record type name in the CloudKit dashboard.
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 Nearby 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
loadCoverPhoto(completion:)
with the following code:func loadCoverPhoto(completion:(photo: UIImage!) -> ()) { // 1 dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)){ var image: UIImage! // 2 let coverPhoto = self.record.objectForKey("CoverPhoto") as CKAsset! if let asset = coverPhoto { // 3 if let url = asset.fileURL { let imageData = NSData(contentsOfFile: url.path!)! // 4 image = UIImage(data: imageData) } } // 5 completion(photo: image) } } |
This method loads the image from the asset attribute as follows:
- Although you download the asset at the same time that you retrieve the rest of the record, you want to load the image asynchronously, so wrap everything in a dispatch_async block.
- Assets are stored in CKRecord as instances of CKAsset, so cast appropriately.
- Load the image data from the local file URL provided by the asset.
- Use the image data to create an instance of
UIImage
. - Execute the completion callback with the retrieved image.
Build and run. The establishment images should now appear:
There are two gotchas with CloudKit assets:
- 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.
- Retrieving assets can negatively impact performance because the assets are downloaded at the same time the rest of the record data is. If your app makes heavy use of assets, you should store references to a different type of record that holds just the asset.
Where To Go From Here?
The app is now able to download Establishment records and load their details and photos into the table view. You can take what’s been built so far and enhance it in several ways:
- Allow the user to add their own pictures, notes, reviews, and complaints. This will help 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 attributes 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, such the ability to receive data during the operation instead of all at the same time, once the operation is complete.
- Provide caching and synchronization so the app remains responsive offline and keeps the content up to date when it reconnects to a network.
0 comments:
Post a Comment