Sqd cloudkit
Agent Skills for SQLiteData (Point-Free's GRDB-based SwiftData replacement with CloudKit sync). Covers @Table, fetch wrappers, queries, migrations, and SyncEngine.
npx -y skills add sitapix/sqlitedata-swift-skills --skill sqd-cloudkitAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when implementing CloudKit sync with SQLiteData — covers SyncEngine setup, sharing records, SyncMetadata queries, backwards-compatible migrations, schema constraints, account changes, and testing sync. NOT for core @Table/@FetchAll patterns (use core) or error lookup (use diag)
SKILL.md
13.1 KB, ~3.2k tokens by cl100k_base, as published. Nobody here has run it
SQLiteData CloudKit Synchronization
Complete guide for CloudKit sync with SQLiteData's SyncEngine.
Architecture
SyncEngine wraps Apple's CKSyncEngine (iOS 17+) and:
- Monitors local SQLite changes via database triggers
- Sends local changes to CloudKit (private + shared databases)
- Receives remote changes and applies them locally
- Manages a separate metadatabase (
sqlitedata_icloud_metadata.sqlite) for CloudKit metadata - Handles field-level conflict resolution ("last edit wins" per column)
1. Project Setup (Xcode)
Before any code:
- Enable iCloud capability → check CloudKit (see
/skill sqd-cloudkit-setupStep 1) - Enable Background Modes → check Remote notifications (see
/skill sqd-cloudkit-setupStep 2) - Add
CKSharingSupported = trueto Info.plist (if sharing — see/skill sqd-sharing) - Before shipping: deploy schema to production (see
/skill sqd-cloudkit-setupStep 3)
2. SyncEngine Initialization
@main
struct MyApp: App {
@State var syncDelegate = MySyncDelegate()
init() {
try! prepareDependencies {
$0.defaultDatabase = try appDatabase()
$0.defaultSyncEngine = try SyncEngine(
for: $0.defaultDatabase,
tables: RemindersList.self, Reminder.self, Tag.self, ReminderTag.self,
privateTables: UserPreferences.self, // Not shareable
containerIdentifier: "iCloud.com.example.app", // nil = from entitlements
startImmediately: true, // default
delegate: syncDelegate,
logger: Logger(subsystem: "MyApp", category: "CloudKit")
)
}
}
}
Key distinction:
tables:— Synced AND shareable with other iCloud usersprivateTables:— Synced but NOT shareable (private database only)
3. Schema Requirements for CloudKit
Globally Unique Primary Keys (REQUIRED)
Your @Table UUID primary key becomes a CKRecord.ID record name in CloudKit — ASCII, max 255 chars, unique per zone (see /skill sqd-sharing for the underlying CloudKit constraints).
-- CORRECT: UUID primary key with ON CONFLICT REPLACE
CREATE TABLE "items" (
"id" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT (uuid()),
...
) STRICT
-- WRONG: Integer autoincrement (conflict across devices)
CREATE TABLE "items" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
...
)
Primary Key on EVERY Synced Table (REQUIRED)
Even join tables need a single (non-compound) primary key:
CREATE TABLE "reminderTags" (
"id" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT (uuid()),
"reminderID" TEXT NOT NULL REFERENCES "reminders"("id") ON DELETE CASCADE,
"tagID" TEXT NOT NULL REFERENCES "tags"("id") ON DELETE CASCADE
) STRICT
NO Uniqueness Constraints (REQUIRED)
Tables with UNIQUE constraints (other than primary key) cannot be synchronized. SyncEngine throws an error on initialization if detected.
Workaround: Make the unique column the primary key itself:
@Table
struct RemindersListAsset {
@Column(primaryKey: true)
let remindersListID: RemindersList.ID // Acts as both PK and FK
var coverImage: Data?
}
Foreign Key Rules
- Supported
ON DELETEactions:CASCADE,SET NULL,SET DEFAULT - NOT supported:
RESTRICT,NO ACTION(throws error) - SyncEngine handles out-of-order records (caches children until parent arrives)
NOT NULL Columns MUST Have ON CONFLICT REPLACE
-- CORRECT
"position" INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 0
-- WRONG (will fail when older devices sync records without this column)
"position" INTEGER NOT NULL DEFAULT 0
Reserved CloudKit Field Names (DO NOT USE)
These are CKRecord system metadata fields (see /skill sqd-sharing for full list). Do not use as column names:
creationDate, creatorUserRecordID, etag, lastModifiedUserRecordID, modificationDate, modifiedByDevice, recordChangeTag, recordID, recordType
4. Backwards-Compatible Migrations
Adding Tables — Safe
New tables are safe. Unrecognized records from newer devices are cached until the table exists.
Adding Columns — Use ON CONFLICT REPLACE or nullable
-- Option A: NOT NULL with ON CONFLICT REPLACE + default
ALTER TABLE "remindersLists"
ADD COLUMN "position" INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 0
-- Option B: Nullable (when no sensible default exists)
ALTER TABLE "remindersLists"
ADD COLUMN "groupID" TEXT REFERENCES "groups"("id")
DISALLOWED Migrations
CloudKit schemas are additive-only once deployed to production (see /skill sqd-cloudkit-setup Step 3):
- Removing columns
- Renaming columns
- Renaming tables
5. Attaching Metadatabase
To query SyncMetadata (CloudKit record data), attach in prepareDatabase:
configuration.prepareDatabase { db in
try db.attachMetadatabase()
}
This enables joining SyncMetadata to your tables to access CKRecord metadata and CKShare data (see /skill sqd-sharing).
6. SyncMetadata Table
@Table("sqlitedata_icloud_metadata")
public struct SyncMetadata: Hashable, Identifiable, Sendable {
public struct ID: Hashable, Sendable {
public let recordPrimaryKey: String
public let recordType: String
}
public let id: ID
public var recordPrimaryKey: String { id.recordPrimaryKey }
public var recordType: String { id.recordType }
public let zoneName: String
public let ownerName: String
public let recordName: String
public let parentRecordID: ParentID?
public let lastKnownServerRecord: CKRecord?
public let share: CKShare?
public var hasLastKnownServerRecord: Bool
public var isShared: Bool
public let userModificationTime: Int64
}
Joining SyncMetadata to Your Tables
@FetchAll(
RemindersList
.leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($1.id) }
.select {
Row.Columns(
remindersList: $0,
isShared: $1.isShared ?? false,
share: $2.share
)
}
)
var rows
Use $0.syncMetadataID — available on all PrimaryKeyedTable types — to join.
Fetching CKRecord for a Record
let serverRecord = try database.read { db in
try SyncMetadata
.find(remindersList.syncMetadataID)
.select(\.lastKnownServerRecord)
.fetchOne(db) ?? nil
}
7. Sharing Records
SQLiteData wraps CloudKit's sharing API (CKShare, UICloudSharingController). For Apple's underlying sharing model — record zones vs hierarchies, participant management, permission types, and UICloudSharingController sample code — see /skill sqd-sharing.
Share a Record
@Dependency(\.defaultSyncEngine) var syncEngine
let sharedRecord = try await syncEngine.share(
record: remindersList,
configure: { share in
share.publicPermission = .readOnly
// or .readWrite, .none
}
)
// sharedRecord.share is the CKShare
Unshare
try await syncEngine.unshare(record: remindersList)
Accept Incoming Share (SceneDelegate)
When a user taps a share URL, CloudKit provides CKShare.Metadata to your app delegate. SQLiteData simplifies acceptance — for the full CloudKit acceptance flow, see /skill sqd-sharing.
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@Dependency(\.defaultSyncEngine) var syncEngine
func windowScene(
_ windowScene: UIWindowScene,
userDidAcceptCloudKitShareWith metadata: CKShare.Metadata
) {
Task { try await syncEngine.acceptShare(metadata: metadata) }
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let metadata = connectionOptions.cloudKitShareMetadata else { return }
Task { try await syncEngine.acceptShare(metadata: metadata) }
}
}
Write Permission Errors
do {
try await database.write { db in
try Reminder.find(id).update { $0.title = "New" }.execute(db)
}
} catch let error as DatabaseError where error.message == SyncEngine.writePermissionError {
// User doesn't have write permission on this shared record
}
8. SyncEngine State Observation
All observable in SwiftUI:
@Dependency(\.defaultSyncEngine) var syncEngine
syncEngine.isRunning // Bool
syncEngine.isSynchronizing // Bool (sending OR fetching)
syncEngine.isSendingChanges // Bool
syncEngine.isFetchingChanges // Bool
Usage in UI:
if syncEngine.isSynchronizing {
ProgressView()
}
9. SyncEngine Control
// Manual sync
try await syncEngine.start()
syncEngine.stop()
try await syncEngine.fetchChanges(options)
try await syncEngine.sendChanges(options)
try await syncEngine.syncChanges() // fetch + send
// Delete all local data (e.g., on account change)
try await syncEngine.deleteLocalData()
10. Account Change Handling (SyncEngineDelegate)
@MainActor
@Observable
class MySyncDelegate: SyncEngineDelegate {
var isDeleteLocalDataAlertPresented = false
func syncEngine(
_ syncEngine: SyncEngine,
accountChanged changeType: CKSyncEngine.Event.AccountChange.ChangeType
) async {
switch changeType {
case .signIn:
break
case .signOut, .switchAccounts:
isDeleteLocalDataAlertPresented = true
@unknown default:
break
}
}
}
// In view:
.alert("Reset local data?", isPresented: $delegate.isDeleteLocalDataAlertPresented) {
Button("Reset", role: .destructive) {
Task { try await syncEngine.deleteLocalData() }
}
}
Default behavior (no delegate): Auto-deletes local data on sign out.
11. Triggers + Sync Awareness
Skip trigger actions during sync using SyncEngine.isSynchronizing:
// StructuredQueries builder
Model.createTemporaryTrigger(
after: .insert { new in ... }
when: { _ in !SyncEngine.$isSynchronizing }
)
// Raw SQL
#sql("""
CREATE TEMPORARY TRIGGER "..."
AFTER DELETE ON "..."
FOR EACH ROW WHEN NOT \(SyncEngine.$isSynchronizing)
BEGIN ... END
""")
When to use: Triggers that set updatedAt timestamps or app-specific side effects.
When NOT to use: FTS index triggers should run regardless of sync source.
12. Assets (BLOB Columns)
BLOB columns are automatically converted to CKAssets. Best practice: separate table for large data:
@Table
struct RemindersListAsset {
@Column(primaryKey: true)
let remindersListID: RemindersList.ID
var coverImage: Data?
}
13. Primary Key Migration (Integer → UUID)
For existing apps with integer primary keys:
migrator.registerMigration("Migrate to UUID primary keys") { db in
try SyncEngine.migratePrimaryKeys(
db,
tables: Reminder.self, RemindersList.self, Tag.self
)
}
This handles: UUID generation (deterministic via MD5), data preservation, foreign key updates, index/trigger recreation.
14. Testing & Previews with SyncEngine
Bootstrap helper pattern:
extension DependencyValues {
mutating func bootstrapDatabase(
syncEngineDelegate: (any SyncEngineDelegate)? = nil
) throws {
defaultDatabase = try appDatabase()
defaultSyncEngine = try SyncEngine(
for: defaultDatabase,
tables: RemindersList.self, Reminder.self,
delegate: syncEngineDelegate
)
}
}
// App: try! prepareDependencies { try $0.bootstrapDatabase(syncEngineDelegate: delegate) }
// Test: @Suite(.dependencies { try! $0.bootstrapDatabase() })
// Preview: let _ = try! prepareDependencies { try $0.bootstrapDatabase() }
15. Conflict Resolution
- Strategy: field-wise last edit wins (per column, not per record)
- Each column edit is timestamped
- When merging conflicts, the most recently edited column value wins
- No CRDT support (may come in future)
16. Simulator Limitations
Simulators don't receive push notifications (the remote-notification background mode — see /skill sqd-cloudkit-setup Step 2), so:
- Changes don't auto-sync from CloudKit to simulator
- Force sync: kill and relaunch the app, or use
syncEngine.syncChanges()
Common CloudKit Mistakes
- Integer primary keys — Must be UUID TEXT for distributed sync (see
/skill sqd-sharing) - UNIQUE constraints on non-PK columns — Not allowed with sync
- NOT NULL without ON CONFLICT REPLACE — Breaks cross-version sync
- RESTRICT/NO ACTION foreign keys — Not supported
- Editing deployed migrations — Always add new migrations (see
/skill sqd-cloudkit-setupStep 3) - Removing/renaming columns — Not allowed with distributed schema
- Not attaching metadatabase — Required to query SyncMetadata
- Using reserved CloudKit field names as column names
Apple Documentation Skills
For Apple's CloudKit documentation (no web search needed):
/skill sqd-cloudkit-setup— iCloud capability, background modes, schema deployment/skill sqd-sharing— CKShare, CKRecord.ID, UICloudSharingController, permissions/skill sqd-swiftdata-sync— SwiftData sync (for comparison/migration)
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.