CloudKit Tutorial: Getting Started

In this CloudKit tutorial, you’ll learn how to add and query data in iCloud from your app, as well as how to manage that data using the CloudKit dashboard. By Andy Pereira.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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!

Loading images in CloudKit

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:

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

Next, add a new field to Establishment:

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:

CloudKit dashboard record name

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:

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:

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.

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.