Getting started with CloudKit
CloudKit is an easy way to store data in the cloud, sync between multiple devices, and share it between the app’s users. This week we will learn how to start using CloudKit in the app to save and fetch data from the cloud and sync between multiple user devices.
The all-in-one macOS app that enhances App Store Connect, supercharging your app updates, localization, and ASO with AI-powered tools. Save 25% and try now!
Basics
First, to start using CloudKit in the app, we need to enable it in the project Signing and Capabilities section. Here we can create a default container for the app. A container is a space in the cloud that stores all of your saved data. You can use a container per application or a single container to share data between multiple apps.
Every container has a public, private and shared database. The public database is accessible for any user of the app. Every user of the app has a private database that lives in the personal iCloud and counts towards iCloud storage. You can use the shared database to fetch data shared with the user.
import CloudKit
import os
final class CloudKitService {
private static let logger = Logger(
subsystem: "com.aaplab.fastbot",
category: String(describing: CloudKitService.self)
)
func checkAccountStatus() async throws -> CKAccountStatus {
try await CKContainer.default().accountStatus()
}
}
@MainActor final class OnboardingViewModel: ObservableObject {
private static let logger = Logger(
subsystem: "com.aaplab.fastbot",
category: String(describing: OnboardingViewModel.self)
)
@Published private(set) var accountStatus: CKAccountStatus = .couldNotDetermine
private let cloudKitService = CloudKitService()
func fetchAccountStatus() async {
do {
accountStatus = try await cloudKitService.checkAccountStatus()
} catch {
Self.logger.error("\(error.localizedDescription, privacy: .public)")
}
}
}
Before saving or fetching data from the CloudKit, we should check if the user has logged in an Apple ID and enabled iCloud Drive in the settings.
import SwiftUI
struct OnboardingView: View {
@StateObject private var viewModel = OnboardingViewModel()
@State private var accountStatusAlertShown = false
@Environment(\.dismiss) var dismiss
var body: some View {
Button("startUsingApp") {
if viewModel.accountStatus != .available {
accountStatusAlertShown = true
} else {
dismiss()
}
}
.alert("iCloudAccountDisabled", isPresented: $accountStatusAlertShown) {
Button("cancel", role: .cancel, action: {})
}
.task {
await viewModel.fetchAccountStatus()
}
}
}
Saving data
We need to define a schema for record types we want to store on CloudKit. Go to the Signing and Capabilities tab on the project settings page and press the CloudKit Console button. It should open the browser with CloudKit dashboard, where you can find schema setup in the navigation menu. Press the record types button, and create a new one that we want to store and fetch.
struct Fasting: Hashable {
var start: Date
var end: Date
var goal: TimeInterval
}
enum FastingRecordKeys: String {
case type = "Fasting"
case start
case end
case goal
}
extension Fasting {
var record: CKRecord {
let record = CKRecord(recordType: FastingRecordKeys.type.rawValue)
record[FastingRecordKeys.goal.rawValue] = goal
record[FastingRecordKeys.start.rawValue] = start
record[FastingRecordKeys.end.rawValue] = end
return record
}
}
In the example above, you see the simple Fasting value type that I want to store on CloudKit. CloudKit provides us CKRecord type representing items in the CloudKit database. Usually, we need to implement a converter from/to CKRecord for our custom types.
extension CloudKitService {
func save(_ record: CKRecord) async throws {
try await CKContainer.default().privateCloudDatabase.save(record)
}
}
And now, we can finally create a form to populate fasting record data and save it to the private database of the current user on CloudKit.
@MainActor final class NewFastingViewModel: ObservableObject {
private static let logger = Logger(
subsystem: "com.aaplab.fastbot",
category: String(describing: NewFastingViewModel.self)
)
@Published var fasting: Fasting = .init(
start: .now,
end: .now,
goal: 16 * 3600
)
@Published private(set) var isSaving = false
private let cloudKitService = CloudKitService()
func save() async {
isSaving = true
do {
try await cloudKitService.save(fasting.record)
} catch {
Self.logger.error("\(error.localizedDescription, privacy: .public)")
}
isSaving = false
}
}
struct NewFastingRecord: View {
@StateObject private var viewModel = NewFastingViewModel()
@Environment(\.dismiss) var dismiss
var body: some View {
Form {
Section {
DatePicker("start", selection: $viewModel.fasting.start)
DatePicker("end", selection: $viewModel.fasting.end)
}
Section {
Picker("goal", selection: $viewModel.fasting.goal) {
ForEach([16, 18, 23], id: \.self) { hours in
Text(String(hours))
.tag(hours * 3600)
}
}
}
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("save") {
Task {
await viewModel.save()
dismiss()
}
}.disabled(viewModel.isSaving)
}
}
.navigationTitle("newFasting")
}
}
Fetching data
Now we can learn how to fetch data from the CloudKit. First, we should update our schema by adding indexes marking fields in our records queryable and sortable. Let’s open the CloudKit dashboard and go to Schema -> Indexes. Here we should create indexes for all the fields we want to fetch or sort.
extension Fasting {
init?(from record: CKRecord) {
guard
let start = record[FastingRecordKeys.start.rawValue] as? Date,
let end = record[FastingRecordKeys.end.rawValue] as? Date,
let goal = record[FastingRecordKeys.goal.rawValue] as? TimeInterval
else { return nil }
self = .init(start: start, end: end, goal: goal)
}
}
In the example above, we implemented another converter from an instance of the CKRecord type. Let’s move forward and implement a method on the CloudKitService type to fetch the records in the provided date interval.
extension CloudKitService {
func fetchFastingRecords(in interval: DateInterval) async throws -> [Fasting] {
let predicate = NSPredicate(
format: "\(FastingRecordKeys.start.rawValue) >= %@ AND \(FastingRecordKeys.end.rawValue) <= %@",
interval.start as NSDate,
interval.end as NSDate
)
let query = CKQuery(
recordType: FastingRecordKeys.type.rawValue,
predicate: predicate
)
query.sortDescriptors = [.init(key: FastingRecordKeys.end.rawValue, ascending: true)]
let result = try await CKContainer.default().privateCloudDatabase.records(matching: query)
let records = result.matchResults.compactMap { try? $0.1.get() }
return records.compactMap(Fasting.init)
}
}
And now we are ready to implement a view showing the fasting history.
@MainActor final class FastingHistoryViewModel: ObservableObject {
private static let logger = Logger(
subsystem: "com.aaplab.fastbot",
category: String(describing: FastingHistoryViewModel.self)
)
@Published var interval: DateInterval = .init(
start: .now.addingTimeInterval(-30 * 34 * 3600),
end: .now
)
@Published private(set) var history: [Fasting] = []
@Published private(set) var isLoading = false
private let cloudKitService = CloudKitService()
func fetch() async {
isLoading = true
do {
history = try await cloudKitService.fetchFastingRecords(in: interval)
} catch {
Self.logger.error("\(error.localizedDescription, privacy: .public)")
}
isLoading = false
}
}
struct FastingHistoryView: View {
@StateObject private var viewModel = FastingHistoryViewModel()
var body: some View {
List(viewModel.history, id: \.self) { fasting in
VStack(alignment: .leading) {
Text(fasting.start, style: .time)
Text(fasting.end, style: .time)
}
}
.redacted(reason: viewModel.isLoading ? .placeholder : [])
.refreshable {
await viewModel.fetch()
}
.task {
await viewModel.fetch()
}
}
}
Conclusion
CloudKit provides us with development and production environments. While developing an app and running it in the debug mode, you automatically use the development environment. Before publishing the app on TestFlight or App Store, you should deploy schema to the production environment in the CloudKit dashboard.
This week we learned the basics of storing and fetching data in the CloudKit. Now you know how to sync the data between the user devices. Next week we will learn how to implement data sharing between app users via CloudKit. I hope you enjoy the post. Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading, and see you next week!