MYCloudKit is a framework designed to simplify and automate CloudKit syncing, deletion, and fetching operations for your app. This guide explains how to integrate and use the various features of MYCloudKit.
To sync your models with CloudKit using MYCloudKit, you must conform them to the MYRecordConvertible protocol. This enables the sync engine to understand how to convert your custom data types into CKRecords.
- Conform Your Model to
MYRecordConvertible
Add protocol conformance to your struct or class.
- Implement Record Identification
myRecordID: A unique identifier (e.g., UUID or your model’s primary key).myRecordType: The record type (e.g., "Task", "Note").
- Group Records with
myRootGroupID(optional)
This lets you group related records (like all tasks in a project) into a CloudKit zone. Especially useful when sharing.
If you project has tasks, tags, subtasks and more other such models, make sure everyone's
myRootGroupIDis theprojectID.
- Define Hierarchies with
myParentID(optional)
Use this to model parent-child relationships like folders and files.
If A is the parent of B and B is the parent of C, sharing A would share A, B and C. Sharing B would only share B and C.
Prefer
.reference(...)inmyPropertiesunless you specifically want record-level sharing.
- Map Properties to CloudKit-Compatible Values
Use the myProperties dictionary to define each field using MYRecordValue (e.g. .string, .bool, .reference, etc.).
CloudKit reserves
creationDate,creatorUserRecordID,lastModifiedUserRecordID,modificationDate,recordChangeTag,recordID, andrecordTypefor record metadata. MYCloudKit fails with a descriptive assertion ifmyPropertiescontains one of these keys. Rename the mapped key—for example, usecreatedAtinstead ofcreationDate.
struct Task: MYRecordConvertible {
let id: String
let title: String
let isDone: Bool
let project: Project
var myRecordID: String { id }
var myRecordType: String { "Task" }
var myRootGroupID: String? { project.id }
var myProperties: [String: MYRecordValue] {
[
"title": .string(title),
"isDone": .bool(isDone),
"project": .reference(project, deleteRule: .deleteSelf)
]
}
}✅ With this setup,
MYCloudKitknows how to save, update, delete, and share your Task model in CloudKit.
- Set Up MYSyncEngine
Before you start syncing, initialize the MYSyncEngine, which is responsible for managing all CloudKit operations:
let syncEngine = MYSyncEngine()
syncEngine.delegate = self // Implement MYSyncDelegate to handle syncing and fetchingBy default, the engine fetches and subscribes to changes in both the private and shared CloudKit databases. If your app does not use sharing, limit it to the private database to avoid unnecessary CloudKit work:
let syncEngine = MYSyncEngine(databaseScopes: [.private])You can likewise use databaseScopes: [.shared] for a shared-only fetch configuration.
The public database is not supported.
Note: If you're going to be fetching/syncing from outside the main app target, make sure you provide
userDefaultsSuiteNameof the App Group so we can fetch correctly and efficiently.
Provide the correct
containerIdentifierif you're not using the default one.
- Syncing Records
To sync records, call the sync(_:) method on the MYSyncEngine. This will enqueue the record for syncing with CloudKit. The engine automatically handles uploading the record, retrying if necessary, and ensuring dependent records are synced in the correct order.
Example of syncing a Task record:
let task = Task(id: "task123", title: "Finish homework", isDone: false)
syncEngine.sync(task)It is your responsibility to sync data sensibly. Don't sync a newly created Task first and then the Project it belongs to (provided it has never been synced) which is conceptually incorrect. A new Project should be synced first and then the newly created Task. This is to ensure that Project that the Task to reference in CloudKit is already present before it (either in the queue or in CloudKit itself). Currently the logic is that the last write wins, in a future commit we will allow you to resolve the conflict via the
MYSyncDelegate.
- Deleting Records
To delete records, use the delete(_:) method. You can delete either a single record or an entire group of records (such as a Task and all its subtasks). The engine ensures dependent records are deleted in the right order and also handles zone deletion if needed.
Example of deleting a Task:
syncEngine.delete(task)By default CloudKit doesn't cascade delete all records that may reference a task. If you want this, we have exposed a parameter
shouldDeleteChildRecordswhich defaults tofalse. You can set this totrue, if you desire that behaviour.
- Fetching Records
To fetch records from CloudKit, use the fetch() method. This fetches records modified or created since the last sync from the database scopes configured when initializing the engine, ensuring your app stays up-to-date.
Example of fetching records:
await syncEngine.fetch()You can observe the fetchState to track whether the fetch operation is in progress, completed, or failed.
- Handle Record Relationships
MYCloudKit supports hierarchical relationships between records. For example, a Task may reference a Project record. To ensure proper syncing, define the correct parent-child relationships using myParentID and myRootGroupID in your MYRecordConvertible models.
- Use CloudKit Sharing
For apps that support sharing, MYCloudKit integrates with CloudKit’s CKShare feature. You can create shares at the record level (for individual records and their nested records using a proper child-parent hierarchy) or zone level (for whole groups of records).
let (share, container) = try await syncEngine.createShare(with: "Shared Task List", for: task)
let controller = UICloudSharingController(share: share, container: container)
// present that controller or wrap it in a UIViewControllerRepresentable :P⚠ Note: Don't spend hours debugging why sharing isn't working. Add
CKSharingSupportedin your info.plist and set it totrue/YES.
To accept a share, make sure you have implemented the following function in your SceneDelegate , that is all that is required to enable sync. Pretty simple, right?
func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
Task {
try await syncEngine.acceptShare(
cloudKitShareMetadata: cloudKitShareMetadata
)
}
}- MYSyncDelegate
Don't forget to conform to the MYSyncDelegate protocol. This delegate handles all sync-related communication between your app’s local storage and CloudKit.
You’ll implement methods to:
- Save and delete synced records locally
- Remove entire record groups (zones)
- Recover from sync failures by correcting broken records
- Provide the list of record types your app uses (in order of dependency)
The most important method is:
func syncableRecordTypesInDependencyOrder() -> [MYRecordType]This function tells MYCloudKit the order in which your records should be given back to you in order to save. The order must respect reference and parent-child dependencies.
If a record (e.g. Task) references another record (e.g. Project), then Project must appear before Task in the array. So when you receive the array of records to save, we'll send [Project] first to you to save and then the [Task] so that when Task is referencing the Project, it is locally present in your database! func syncableRecordTypesInDependencyOrder() -> [MYRecordType] { return [ "Project", // Zone root / parent "Task", // References Project "Subtask" // References Task ] }
func syncableRecordTypesInDependencyOrder() -> [MYRecordType] {
return [
"Project", // Root
"Tag", // Tasks may reference them so they're before Task
"Task", // References Project and Tags
"Subtask" // References Task
]
}- Handling Sync Errors and Retries
MYCloudKit automatically retries failed sync operations up to a configurable limit (maxRetryAttempts provided while initialising CKSyncEngine). If a record fails to sync, you can inspect the error and decide whether to fix the issue and retry or exclude the record.
You can implement the handleUnsyncableRecord method in MYSyncDelegate to customize error handling:
func handleUnsyncableRecord(recordID: String, recordType: String, reason: String, error: Error) -> [any MYRecordConvertible]? {
// Return a fixed version of the record for retrying, or nil to skip syncing
return nil
}- Receiving Push Notifications
To ensure your app stays in sync with remote changes (e.g., when another device modifies shared records), make sure to call .fetch() on the MYSyncEngine when your app receives a CloudKit push notification.
This is especially important for background and foreground updates.
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any]
) async -> UIBackgroundFetchResult {
await AppState.shared.syncEngine.fetch()
return .newData
}Note: These push notifications aren't received on simulator. Works only on a real device. This allows your app to stay updated by fetching the latest changes from CloudKit as soon as a silent push notification is received. MYCloudKit automatically sets up silent push notifications for changes in the configured private and/or shared databases.
| Icon | App | Developer | Category |
|---|---|---|---|
| Tasks - To Do Lists & Reminder | Msquare Labs Technologies Private Limited | Productivity | |
| Pediapal: Child Health Monitor | Aberfeldy Studios, LLC | Health & Fitness | |
| Art of Fauna: Cozy Puzzles | Klemens Strasser | Games | |
| Art of Flora: Cozy Puzzles | Klemens Strasser | Games | |
| PocketShelf: TBR Book Tracker | Klemens Strasser | Book |
Is your app built with MYCloudKit? Add its numeric App Store ID to
app.json:
{
"appStoreIds": [
1234567890
]
}The app's name, developer, icon, category, and link are fetched from Apple's public App Store lookup API. Open a pull request with the new ID; GitHub validates it and updates the showcase on the pull request automatically. A post-merge refresh handles contributions from forks, which GitHub does not let the repository bot modify directly.
Bad Habit Tracker App is a small
example project demonstrating MYCloudKit.
MYCloudKit is crafted to simplify CloudKit syncing so you can focus on building great apps and not wrestling with APIs as I have for the initial years.
If you found this helpful or you’re using it in your app, I’d love to hear from you!
Feel free to reach out on X (Twitter), Instagram, or wherever you hang out.
I’m probably there as @mufasayc.