CloudKit: A Concise Tutorial
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!