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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
CloudKit Tutorial: Getting Started
30 mins
- Why CloudKit?
- Simplicity
- Trust
- Cost
- Introducing BabiFüd
- Getting Started
- Entitlements and Containers
- Troubleshooting iCloud Setup in Xcode
- Introducing the CloudKit Dashboard
- Adding the Establishment Record Type
- Querying Establishment Records
- Working With Binary Assets
- Relationships
- Troubleshooting Queries
- Where to Go From Here?
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:
- 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. - Check to make sure the asset
coverPhoto
exists and has afileURL
. - Download the image’s binary data.
- Use the image data to create an instance of
UIImage
. - Execute the completion callback with the retrieved image. Note that the
defer
block gets executed regardless of whichreturn
is executed. For example, if there is no image asset, thenimage
never gets set upon the return and no image appears for the restaurant.
Build and run. The establishment images should now appear. Great job!
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 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:
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.