CloudKit: A Concise Tutorial

Paulo Sonzzini
Apple Developer Academy | Mackenzie
4 min readJun 17, 2024

Introduction

CloudKit is an Apple framework that uses iCloud for data persistence, enabling multiple devices to be up to date and efficient syncing, with up to 1PB of public data storage.

NOTE: CloudKit integration is exclusive for developers enrolled in the Apple Developer Program.

The first step to using CloudKit in your app is making sure you are signed in with your Apple Developer Program account. With it, you'll be able to assign CloudKit usage in your application's Signing & Capabilities tab.

Scrolling down to the iCloud section, you can check the CloudKit box and create a container to publish data to.
NOTE: As of June 2024, there is no way to delete a container, so double check the name of the container, as well as it's usage. Checking the box of the container will set it as the default container for the application.

For more help enabling CloudKit in your application, see here.

First Steps

The initial code setup for CloudKit usage is as follows:

import CloudKit

@Observable class CloudController {
let container: CKContainer
let databasePublic : CKDatabase

init() {
self.container = CKContainer.default()
self.databasePublic = container.publicCloudDatabase
}
}

Here, we get the container and it's publicCloudDatabase for use.
The databasePublic variable is where we will have access to methods such as:

  • save(_ record: CKRecord) -> CKRecord
  • delete(withRecordID recordID: CKRecord.ID)
  • fetch()
  • perform(_ query: CKQuery)
  • record(for recordID: CKRecord.ID) -> CKRecord

In this article, all examples will be referring to a Project class. This is it's implementation:

@Observable class Project {
public var projectId: CKRecord.ID?
public var projectName: String

// Used to manage the CKRecord refering to the object created
func getRecord() -> CKRecord {
let projectRecord = CKRecord(recordType: RecordNames.Project.rawValue, recordID: projectId ?? CKRecord(recordType: RecordNames.Project.rawValue, recordID: CKRecord.ID(recordName: UUID().uuidString)).recordID)
projectRecord.setValue(self.projectName, forKey: ProjectFields.projectName.rawValue)

return projectRecord
}

// Used to instantiate project model from a record returned from a CloudKit query
init?(_ record: CKRecord) {
guard let projectName = record[ProjectFields.projectName.rawValue] as? String else { return nil }
self.projectName = projectName
self.pojectId = record.recordID
}

// Used to first instantiate the class and set it up for saving
init(projectName: String) {
self.projectName = projectName
}
}

// Used to identify the CKRecord attribute fields
enum ProjectFields: String {
case projectId
case projectName
}

// Used to identify the CKRecords that exist
enum RecordNames: String {
case Project
}

CRUD

For all posterior examples, they're methods of the CloudController class.

Create

To add a record to your CloudKit's public database, we will use the save method. Below is an example of it's use, saving a Project:

private func createProject(_ projectName: String) async -> Project? {
do {
let project = Project(projectName: projectName)
let projectRecord = project.getRecord()
let record = try await databasePublic.save(projectRecord)
let newProject = Project(record)
return newProject
} catch {
print("Error creating project: \(error)")
return nil
}
}

Read / Fetch

A simple way to fetch records inside your container is using the record(for recordID: CKRecord.ID) -> CKRecord method. It will return a record, that we will then use to instantiate a Project object. Below is an example if it's use:

private func getProject(_ projectName: String) async -> Project? {
let recordId = CKRecord.ID(recordName: projectName)
do {
let record = try await self.databasePublic.record(for: recordId)
return Project(record)
} catch {
print("Record not found: \(error)")
return nil
}
}

Update

Updating a records parameters is made easy with the implementation of the ProjectFields enum and the .setValue method for a CKRecord. Below is an example of updating the name of a Project:

private func updateProjectName(newProjectName: String, project: Project) async -> Void {
do {
let projectRecord = try await self.databasePublic.record(for: project.getRecord().recordID)
projectRecord.setValue(ProjectField.projectName.rawValue, forKey: newProjectName)
let savedRecord = try await self.databasePublic.save(projectRecord)
project.projectName = newProjectName
} catch {
print("Error updating project record: \(error)")
return
}
}

Delete

Deleting a record is as easy as a .deleteRecord, no… really. Below is a deletion example of a Project record

private func deleteProject(_ project: Project) async -> Void {
do {
let record = project.getRecord()
try await self.databasePublic.deleteRecord(withID: record.recordID)
} catch {
print("Error deleting project record: \(error)")
return
}
}

Final Thoughts

This is just a simple and concise way of showing the power and potential that CloudKit brings to your app. There are many other methods to take advantage of, such as predicate usage for query filtration, relationships, etc., and I highly suggest you to take a look at the official Apple iCloud documentation here, and other sample projects here.

Remember, the iCloud CloudKit database console, available on your account, is your friend during the development process, and it'll give important insight as to what is happening during execution.

Thank you, and happy coding!

--

--