Skip to content

A small package to simplify basic CloudKit interactions.

License

Notifications You must be signed in to change notification settings

enhorn/OneCloudyKitty

Repository files navigation

OneCloudyKitty

A small package to simplify basic CloudKit interactions, uses Async/Await and pulls data on a configured time interval.

Usage example

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.
}