13

CloudKit Tutorial: Getting Started [FREE]

 4 years ago
source link: https://www.tuicool.com/articles/JZjIbme
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Update note : Andy Pereira updated this tutorial for Xcode 11, iOS 13 and Swift 5.1. Michael Katz wrote the original.

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.

With CloudKit, users’ private data is completely protected because developers can only access their own private databases and aren’t able to look at any user’s private data.

CloudKit is a good option for iOS-only apps that use a lot of data but don’t require a great deal of server-side logic. In addition, you can use CloudKit for web and server apps.

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 other commercial BaaS (Backend as a Service) offerings, or even rolling your own server.

There are three reasons: Simplicity, trust and cost.

Simplicity

Unlike other back-end solutions, CloudKit requires little setup. You don’t have to choose, configure or install servers. Apple handles security and scaling 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; you import CloudKit 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 that users enter when they set up their devices (or that they enter after setup via the Settings app), there’s no need to build complicated login screens. As long as they are logged in, users can use your app seamlessly. 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’s 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, the cost of running a service is a huge deal for any developer. Even the least-expensive server hosts can’t 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 of public data for free. There’s a handy calculator on Apple’s CloudKit site .

These strengths make the CloudKit service a low-hassle solution 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 restaurant” app. Instead of reviewing restaurants based upon food quality, speed of service or price, users rate child-friendliness. This includes the availability of changing facilities, booster seats and healthy food options.

The app contains two tabs: A list of nearby restaurants and user-generated notes. You can get a glimpse of the app in action below.

babifud_main-1-281x500.png

babifud_detail-281x500.png

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

Use the Download Materials button at the top or bottom of this tutorial to download the starter project.

You’ll have to change the Bundle Identifier and Team of your app before you can start coding. You need to set the team to get the necessary entitlements from Apple, while 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.

babifud_set_bundle_identifier-650x176.png

That takes care of the Bundle Identifier. 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’s a group of public and private databases.

CloudKit-container-diagram.png

To create a container, you first need to enable the iCloud entitlements for your app:

  1. Select the Signing & Capabilities tab in the target editor.
  2. Click on + Capability .
  3. Choose iCloud .
  4. Select your Team from the drop-down.

babifud_add_entitlement-650x208.png

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.

Next, enable CloudKit by checking the CloudKit checkbox in the Services group.

babifud-add-cloudkit-650x140.png

Finally, click on + under Containers to add a new container, if CloudKit didn’t automatically create one for you.

babifud-add-container-650x279.png

In the pop-up, add your bundle identifier.

babifud_container-650x136.png

This creates a default container named iCloud.<your app’s bundle ID> .

Note : At the time of writing, CloudKit containers cannot be deleted. Keep in mind that if you create a new container for this tutorial, it will live on forever in your CloudKit Dashboard.

Troubleshooting iCloud Setup in Xcode

If you see any warnings or errors related to the container ID when creating entitlements, building the project or running the app, here are some troubleshooting tips:

  • If there are any warnings or errors shown in the Steps group in the iCloud section, try pressing the Fix Issue button. You might need to do this a few times.
  • The app’s bundle ID and iCloud containers must match and they must 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. This is why you cannot use com.raywenderlich.BabiFud and you had to change it earlier.
  • For the entitlements to work, the app/bundle ID has to appear 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, 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

Your next step is to create some record types that define the data that your app will use. You can do this using the CloudKit dashboard. Click CloudKit Dashboard , which you can find in the target’s Signing & Capabilities pane, under iCloud or by opening https://icloud.developer.apple.com/dashboard/ in your browser.

babifud-open-cloudkit-dashboard-650x279.png

Note : It sometimes takes a while for a new app’s container to show up in the dashboard. If you don’t see your container right away, wait a few minutes and try again.

Here’s what the dashboard looks like:

cloudkit_website-650x385.png

The CloudKit dashboard consists of six sections:

  • Data
  • Schema
  • Telemetry
  • Usage
  • Logs
  • API Access

The Schema section represents the high-level objects of a CloudKit container: Record Types , Indexes , Security Roles and Subscription Types . You’ll only deal 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. You can think of a record as 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.

Under Data , the Private Database and Public Database sections let you add data to, or search for, data in the databases you have access to. Remember, as a developer you can access all public data, but only your own private data.

The User Records store data, such as name and email, about the current iCloud user.

You use a Record Zone , noted here as the Default Zone , to provide a logical organization to a private database by grouping records together.

Custom zones support atomic transactions by allowing you to save multiple records at the same time before processing other operations. Custom zones are outside the scope of this tutorial.

The API Access 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. The establishments you track each have lots of data: Name, location and availability of child-friendly options. Record types use fields to define the various pieces of data each record contains.

Under Schema , select Record Types and then New Type to add a new record type.

babifud_add_recordType-650x215.png

Name your new record type Establishment , then press Enter .

You’ll see a row of system fields, automatically created for every record type. Each one has a Field Name , a Field Type and an Index .

Of course, you can add your own fields. Start by selecting Add Field , call it name , and set the Field Type to String . Repeat until you have added all these fields:

babifud_fieldNames.png

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

Next, click Edit Indexes . You’re going to add two indexes so that you can query your records:

1. recordName with the index type of QUERYABLE .

2. name with the index type of QUERYABLE .

You need to make recordName queryable by default. You set name to queryable so you can query records based on a specific name.

Click Save Changes at the bottom of the page to save your indexes.

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

babifud_firsttype-650x169.png

You’re now ready to add some sample establishment records to your database. To do this, switch to Data in the drop-down at the top of the dashboard:

babifud_switch_to_data.png

Select _defaultZone under the Public Database 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 drop-down list in the center pane, if it’s not already selected. Then click the New Record button in the bottom detail pane, as shown in the screenshot below:

babifud_add_record-244x500.png

This will create a new, empty Establishment record.

babifud_new_record_entry-650x343.png

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:

babifud_recordsToAdd-650x342.png

You’ll find the images in the materials file containing the starter project, which you downloaded earlier. Look for the Images folder.

Once you’ve saved all the records, the dashboard should look like this. You might need to click the Query Records button to make the records appear:

babifud_addedrecords-650x314.png

For each record, the entered values are the database representation of the data.

On the app side, the data types are different. For example, ChangingTable is an enum. So the specified Int value for changingTable might correspond to a changing table being available in the men’s or women’s room.

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.

To run the app, you need to have an iCloud account that you can use for development. You’ll find instructions here: Creating an iCloud Account for Development.

You’ll also need to enter the iCloud credentials associated with this account into the iOS Simulator: Enter iCloud Credentials Before Running Your App

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

Querying Establishment Records

To select records from a database, you’ll use CKQuery . A CKQuery describes how to find all records of a specific type that match certain criteria. These criteria can be something like “all records with a Name field that starts with ‘M’”, “all records that have booster seats” or “all records within 3 km”.

iOS uses NSPredicate to handle these kinds of expressions. NSPredicate evaluates objects to see if they match the criteria. Predicates are also used in Core Data; they’re a natural fit for CloudKit because the definition of a predicate is simply 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 CKQuery Class Reference contains a detailed list of the functions that CloudKit supports as well as descriptions of how to use them.

In Xcode, open Model.swift . Replace refresh(_:) with the following implementation:

@objc func refresh(_ completion: @escaping (Error?) -> Void) {
  // 1.
  let predicate = NSPredicate(value: true)
  // 2.
  let query = CKQuery(recordType: "Establishment", predicate: predicate)
  establishments(forQuery: query, completion)
}

Here’s what you’ve added:

  1. You create a predicate with the value true . NSPredicate determines how you fetch or filter data; in this case, you’re specifying that a value should just exist.
  2. You add a query to specify which type of record you want and a predicate.

Next, replace establishments(forQuery:_:) with the following:

private func establishments(forQuery query: CKQuery,
    _ completion: @escaping (Error?) -> Void) {
  publicDB.perform(query,
      inZoneWith: CKRecordZone.default().zoneID) { [weak self] results, error in
    guard let self = self else { return }
    if let error = error {
      DispatchQueue.main.async {
        completion(error)
      }
      return
    }
    guard let results = results else { return }
    self.establishments = results.compactMap {
      Establishment(record: $0, database: self.publicDB)
    }
    DispatchQueue.main.async {
      completion(nil)
    }
  }
}

Since you’re passing the CKQuery object here, your public database, publicDB , can perform the query. If you think back to when you created the two Establishment objects in the CloudKit dashboard, you put them in the public database, in the default container. That’s exactly what inZoneWith ’s parameter is specifying here. Now, the database will query any Establishment records that exist in the public database.

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

babifud_initialRun-281x500.png

Things don’t look quite right. You have two items in the table, and if you select one, most of the details are properly loaded.

babifud_initialRun_details-281x500.png

However, the details screen is missing an image. That’s because the images you uploaded are CKAsset s. These require a bit of special handling.

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 NearbyTableViewController ’s table view.

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

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

func loadCoverPhoto(completion: @escaping (_ photo: UIImage?) -> ()) {
  // 1.
  DispatchQueue.global(qos: .utility).async {
    var image: UIImage?
    // 5.
    defer {
      DispatchQueue.main.async {
        completion(image)
      }
    }
    // 2.
    guard 
      let coverPhoto = self.coverPhoto,
      let fileURL = coverPhoto.fileURL 
      else {
        return
    }
    let imageData: Data
    do {
      // 3.
      imageData = try Data(contentsOf: fileURL)
    } catch {
      return
    }
    // 4.
    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 DispatchQueue.async block.
  2. Check to make sure the asset coverPhoto exists and has a fileURL .
  3. Download the image’s binary data.
  4. Use the image data to create an instance of UIImage .
  5. Execute the completion callback with the retrieved image. Note that the 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. The establishment images should now appear. Great job!

babifud_workingimage-281x500.png

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 you download the assets 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.

Relationships

It’s important to understand how you can create a relationship between different record types in CloudKit. To do this, you’re going to add a new record type, Note , and create private records that are not in the public database. These records will belong to an Establishment.

Back in the CloudKit dashboard, add the Note type by going to Schema and selecting New Type . Add the following fields and then save:

babifud_note_type.png

Click Edit Indexes and then click Add Index to make recordName queryable.

Next, add a new field to Establishment:

babifud_noteslist.png

By creating the field notes on Establishment, and establishment on Note, you now have a one-to-many relationship. This means an Establishment can have many notes, but a Note can only belong to one Establishment.

Before you continue, you need to get the Name value of an Establishment record. In the CloudKit dashboard, go back to Data , select Public Database and the type Establishment from the drop-down. Next, click on Query Records . Make a note of the first item’s Name , like below:

babifud_find_name-650x500.png

Next, create a Note record in the CloudKit dashboard, just like you did for the two Establishment records. Still in the Data section of the dashboard, select Note from the Type drop-down. However, change Public Database to Private Database and then select New Record . Now, your record will only be available in your CloudKit database. Then change the following values:

  • For Establishment , enter the value found in the name field.
  • Enter anything you’d like for text .

Before you save, copy the value found in the new note’s Name field to make things easier for the next step.

Your new record should look like this:

babifud_newnote-1-261x500.png

Select Save . Next, query your public Establishments and edit the record whose Name you used for the note. Select the + button, enter the note’s name that you saved from the previous step, then Save . It should look like this:

babifud_added_note_relationship-265x500.png

You now have a public Establishment record that has a relationship to a private Note record! To load the notes, open Note.swift , and replace fetchNotes(_:) with the following:

static func fetchNotes(_ completion: @escaping (Result<[Note], Error>) -> Void) {
  let query = CKQuery(recordType: "Note",
                      predicate: NSPredicate(value: true))
  let container = CKContainer.default()
  container.privateCloudDatabase
    .perform(query, inZoneWith: nil) { results, error in
    
  }
}

This looks similar to how you query and download establishments. However, note that the information is now loading from the privateCloudDatabase instead of the public database. It’s that simple to specify whether you want user-specific or public data in your app.

Next, add the following inside the closure to get the record’s data, like you did earlier for Establishment:

if let error = error {
  DispatchQueue.main.async {
    completion(.failure(error))
  }
  return
}
  
guard let results = results else {
  DispatchQueue.main.async {
    let error = NSError(
      domain: "com.babifud", code: -1,
      userInfo: [NSLocalizedDescriptionKey: "Could not download notes"])
    completion(.failure(error))
  }
  return
}

let notes = results.map(Note.init)
DispatchQueue.main.async {
  completion(.success(notes))
}

This code, however, will only work to get all of the user’s notes loaded. To load a note with a relationship to an establishment, open Establishment.swift , and add the following to the end of init?(record:database:) :

if let noteRecords = record["notes"] as? [CKRecord.Reference] {
  Note.fetchNotes(for: noteRecords) { notes in
    self.notes = notes
  }
}

This will check to see if your establishment has an array of references, and then load only those specific records. Open Note.swift and add the following method:

static func fetchNotes(for references: [CKRecord.Reference],
                       _ completion: @escaping ([Note]) -> Void) {
  let recordIDs = references.map { $0.recordID }
  let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
  operation.qualityOfService = .utility
  
  operation.fetchRecordsCompletionBlock = { records, error in
    let notes = records?.values.map(Note.init) ?? []
    DispatchQueue.main.async {
      completion(notes)
    }
  }
  
  Model.currentModel.privateDB.add(operation)
}

Use CKFetchRecordsOperation to easily load multiple records at once. You create it with a list of IDs and set its quality of service to make sure it runs in the background. Then, set the completion block to pass the fetched notes or an empty array if there’s an error. To run the operation, call add on the private database.

Build and run, then go to the Notes tab. You should see that your note has loaded.

babifud_notes-281x500.png

Also, go to the establishment where you set the note and select Notes . You can see that the other establishment does not load the note.

Note : Up to this point, you didn’t need to be logged in to iCloud for the app to work. If you are having trouble loading notes, log in under Settings/Sign in to your iPhone . Keep in mind, you need to be logged in with the same Apple ID that you used for logging into the CloudKit dashboard. Then, try relaunching the app to see the data load properly.

Troubleshooting Queries

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, that you’ve added them to the default zone and that they have the correct values.

If you need to re-enter the data, you can delete records by clicking the trash icon .

Debugging CloudKit errors can be tricky. CloudKit error messages often don’t contain a tremendous amount of information.

To determine the cause of the error, you can 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.

Note : For a list of error codes that CloudKit can return, read the CKError Reference .

Where to Go From Here?

You can download the final version of the project using the Download Materials button at the top or bottom of this CloudKit tutorial.

You can enhance the app in several ways:

  • Allow the user to add their own pictures and notes.
  • Let the user create new Establishment records that only they can see.
  • Provide caching and synchronization so the app remains responsive offline and keeps the content up to date when it reconnects to a network.

For more information, watch Brian Moakley’s Video Tutorial Series on CloudKit .

You can also check out Apple’s documentation on CloudKit .

With CloudKit, you can take your apps to the next level and beyond with this great Apple-provided back-end API. We can’t wait to see what you make.

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK