A small package to simplify basic CloudKit interactions, uses Async/Await and pulls data on a configured time interval.
The content this tutorial, with some extras, can be found in the package's Example project.
Before we start set up a CloudKit container and selecting it in your Xcode project. This README doesn't cover that generic CloudKit part.
We start by defining our model that we want to store.
// Model fulfilling the protocol `OneRecordable`.
final class SomeEntity: OneRecordable {
var recordID: CKRecord.ID // Required by `OneRecordable`.
var name: String
var age: Int
init(name: String, age: Int) {
self.recordID = Self.generateID() // Has a default implementation in `OneRecordable`.
self.name = name
self.age = age
}
// Required by `OneRecordable`. Can return `nil`.
required init?(_ record: CKRecord) {
guard let name = record["name"] as? String, let age = record["age"] as? Int else { return nil }
self.recordID = record.recordID
self.name = name
self.age = age
}
}
We can then use the model together with an OneCloudController
.
let controller = OneCloudController(database: .public, containerID: "iCloud.SomeTestContainer")
// Interactions with CloudKit will throw when failing.
// So for this example we put everything in a single do/catch.
do {
// The entity will now be saved to CloudKit.
let entity = try await controller.create(entity: SomeEntity(name: "Some entity", age: 41))
// Here we update one of the properties and save the entity.
// Saving, updating and deleting entities return a `discardable` copy of the entity.
entity.name = "Saved entity"
entity = try await controller.save(entity: entity)
// We can update a single property based on a keypath.
entity = try await controller.updateProperty(entity: entity, property: \.age, value: 42)
// We can fetch all entities stored in CloudKit.
// The `getAll()` function has an optional `NSPredicate` parameter for filtering the result.
let entities: [SomeEntity] = try await controller.getAll()
// And we can delete the entity from the CloudKit storage.
try await controller.delete(entity: entity)
} catch let error {
print(error)
}
Let's define a list that automatically updates it's content based on a subscribed entity type.
struct EntitiesList: View {
// The subscriber is `@Observable`.
let subscriber: OneSubscriber<SomeEntity>
var body: some View {
List {
// The observable subscriber has a published array named `entities`.
ForEach(subscriber.entities) { entity in
Text(entity.name)
}
}.refreshable {
do {
// We can manually trigger a refresh of the entities.
try await subscriber.refresh()
} catch let error {
print(error)
}
}
}
}
And now let's display the list in our app.
struct ContentView: View {
let controller = OneCloudController(database: .public, containerID: "iCloud.SomeTestContainer")
let subscriber: OneSubscriber<SomeEntity>
init () {
// Takes an optional `NSPredicate` parameter for filtering the fetched entities.
// Also has an optional time interval for pulling. Defaults to `5` minutes.
subscriber = OneSubscriber<SomeEntity>(controller: controller)
subscriber.start() // Starts pulling data
}
var body: some View {
NavigationStack {
EntitiesList(subscriber: subscriber)
.navigationTitle("My subscribed entities")
}
}
}
There is also a database backed subscriber available, that will keep the database up-to-date with CloudKit.
let subscriber = try OneStoredSubscriber<SomeEntity.StorageModel, SomeEntity>(
containerURL: URL.applicationSupportDirectory.appendingPathComponent("MyApp/database.sqlite"),
controller: OneCloudController(containerID: "iCloud.SomeTestContainer"),
updateModel: { model, entity in
model.name = entity.name
model.age = entity.age
model.changeDate = entity.changeDate
},
updateEntity: { entity, model in
entity.name = model.name
entity.age = model.age
entity.changeDate = model.changeDate
}
)
And a generic reusable stored subscriber as well, where we have typed subscripts in the entity for supported stored types.
The entities will always be stored as OneGenericEntity
in CoudKit, and OneGenericStorageModel
in the database.
let subscriber = try OneGenericStoredSubscriber.genericStoredSubscriber(
containerURL: URL.applicationSupportDirectory.appendingPathComponent("MyApp/database.sqlite"),
controller: OneCloudController(containerID: "iCloud.SomeTestContainer")
)
if let entity = subscriber.entities.first {
entity[string: "value-key"] // Optional String value
entity[integer: "value-key"] // Optional Int value
entity[double: "value-key"] // Optional Double value
entity[date: "value-key"] // Optional Date value
entity[data: "value-key"] // Optional Data value
entity[bool: "value-key", defaultValue: false] // Bool value, `defaultValue` is optionl.
}