Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
7
.claude/skills/axiom-cloudkit-ref/.openskills.json
Normal file
7
.claude/skills/axiom-cloudkit-ref/.openskills.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"source": "CharlesWiltgen/Axiom",
|
||||
"sourceType": "git",
|
||||
"repoUrl": "https://github.com/CharlesWiltgen/Axiom",
|
||||
"subpath": "axiom-codex/skills/axiom-cloudkit-ref",
|
||||
"installedAt": "2026-04-12T08:06:01.776Z"
|
||||
}
|
||||
630
.claude/skills/axiom-cloudkit-ref/SKILL.md
Normal file
630
.claude/skills/axiom-cloudkit-ref/SKILL.md
Normal file
@@ -0,0 +1,630 @@
|
||||
---
|
||||
name: axiom-cloudkit-ref
|
||||
description: Use when implementing 'CloudKit sync', 'CKSyncEngine', 'CKRecord', 'CKDatabase', 'SwiftData CloudKit', 'shared database', 'public database', 'CloudKit zones', 'conflict resolution' - comprehensive CloudKit database APIs and modern sync patterns reference
|
||||
license: MIT
|
||||
compatibility: iOS 10.0+, iPadOS 13.0+, macOS 10.12+
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
last-updated: "2025-12-12"
|
||||
---
|
||||
|
||||
# CloudKit Reference
|
||||
|
||||
**Purpose**: Comprehensive CloudKit reference for database-based iCloud storage and sync
|
||||
**Availability**: iOS 10.0+ (basic), iOS 17.0+ (CKSyncEngine), iOS 17.0+ (SwiftData integration)
|
||||
**Context**: Modern CloudKit sync via CKSyncEngine (WWDC 2023) or SwiftData integration
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Implementing structured data sync to iCloud
|
||||
- Choosing between SwiftData+CloudKit, CKSyncEngine, or raw CloudKit APIs
|
||||
- Setting up public/private/shared databases
|
||||
- Implementing conflict resolution
|
||||
- Debugging CloudKit sync issues
|
||||
- Monitoring CloudKit performance
|
||||
|
||||
**NOT for**: Simple file sync (use `axiom-icloud-drive-ref` instead)
|
||||
|
||||
## Overview
|
||||
|
||||
**CloudKit is for STRUCTURED DATA sync** (records with relationships), not simple file sync.
|
||||
|
||||
Three modern approaches:
|
||||
1. **SwiftData + CloudKit** (Easiest, iOS 17+)
|
||||
2. **CKSyncEngine** (Custom persistence, iOS 17+, WWDC 2023)
|
||||
3. **Raw CloudKit APIs** (Maximum control, more complexity)
|
||||
|
||||
---
|
||||
|
||||
## Approach 1: SwiftData + CloudKit (Recommended)
|
||||
|
||||
**When to use**: iOS 17+ apps with SwiftData models
|
||||
|
||||
**Limitations**:
|
||||
- Private database only (no public/shared)
|
||||
- Automatic sync (less control)
|
||||
- `@Attribute(.unique)` not supported with CloudKit sync — remove if using CloudKit
|
||||
- SwiftData constraints apply
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: SwiftData with CloudKit sync
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
class Task {
|
||||
var title: String
|
||||
var isCompleted: Bool
|
||||
var dueDate: Date
|
||||
|
||||
init(title: String, isCompleted: Bool = false, dueDate: Date) {
|
||||
self.title = title
|
||||
self.isCompleted = isCompleted
|
||||
self.dueDate = dueDate
|
||||
}
|
||||
}
|
||||
|
||||
// Configure CloudKit container
|
||||
let container = try ModelContainer(
|
||||
for: Task.self,
|
||||
configurations: ModelConfiguration(
|
||||
cloudKitDatabase: .private("iCloud.com.example.app")
|
||||
)
|
||||
)
|
||||
|
||||
// That's it! Sync happens automatically
|
||||
```
|
||||
|
||||
**Entitlements required**:
|
||||
- iCloud capability
|
||||
- CloudKit container
|
||||
|
||||
**Use `axiom-swiftdata` skill for SwiftData details**
|
||||
|
||||
---
|
||||
|
||||
## Approach 2: CKSyncEngine (Modern, WWDC 2023)
|
||||
|
||||
**When to use**: Custom persistence (SQLite, GRDB, JSON) with cloud sync
|
||||
|
||||
**Advantages over raw CloudKit**:
|
||||
- Manages fetch/upload cycles automatically
|
||||
- Handles conflicts
|
||||
- Manages account changes
|
||||
- Recommended over manual CKDatabase operations
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT: CKSyncEngine setup
|
||||
import CloudKit
|
||||
|
||||
class SyncManager {
|
||||
let syncEngine: CKSyncEngine
|
||||
|
||||
init() throws {
|
||||
let config = CKSyncEngine.Configuration(
|
||||
database: CKContainer.default().privateCloudDatabase,
|
||||
stateSerialization: loadSyncState(),
|
||||
delegate: self
|
||||
)
|
||||
|
||||
syncEngine = try CKSyncEngine(config)
|
||||
}
|
||||
|
||||
// Implement delegate methods
|
||||
}
|
||||
|
||||
extension SyncManager: CKSyncEngineDelegate {
|
||||
// Handle events
|
||||
func handleEvent(_ event: CKSyncEngine.Event, syncEngine: CKSyncEngine) async {
|
||||
switch event {
|
||||
case .stateUpdate(let stateUpdate):
|
||||
saveSyncState(stateUpdate.stateSerialization)
|
||||
|
||||
case .accountChange(let change):
|
||||
handleAccountChange(change)
|
||||
|
||||
case .fetchedDatabaseChanges(let changes):
|
||||
applyDatabaseChanges(changes)
|
||||
|
||||
case .fetchedRecordZoneChanges(let changes):
|
||||
applyRecordChanges(changes)
|
||||
|
||||
case .sentRecordZoneChanges(let changes):
|
||||
handleSentChanges(changes)
|
||||
|
||||
case .willFetchChanges, .didFetchChanges,
|
||||
.willSendChanges, .didSendChanges:
|
||||
// Optional lifecycle events
|
||||
break
|
||||
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Next batch of changes to send
|
||||
func nextRecordZoneChangeBatch(
|
||||
_ context: CKSyncEngine.SendChangesContext,
|
||||
syncEngine: CKSyncEngine
|
||||
) async -> CKSyncEngine.RecordZoneChangeBatch? {
|
||||
// Return pending local changes
|
||||
let pendingChanges = getPendingLocalChanges()
|
||||
return CKSyncEngine.RecordZoneChangeBatch(
|
||||
pendingSaves: pendingChanges,
|
||||
recordIDsToDelete: []
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key concepts**:
|
||||
- **State serialization**: Persist sync state between app launches
|
||||
- **Events**: Delegate receives events for changes
|
||||
- **Batches**: You provide pending changes, engine uploads them
|
||||
- **Automatic conflict resolution**: Engine handles basic conflicts
|
||||
|
||||
---
|
||||
|
||||
## Approach 3: Raw CloudKit APIs (Legacy)
|
||||
|
||||
**When to use**: Only if CKSyncEngine doesn't fit (rare)
|
||||
|
||||
**Core types**:
|
||||
- `CKContainer` — Entry point
|
||||
- `CKDatabase` — Public/private/shared scope
|
||||
- `CKRecord` — Individual data record
|
||||
- `CKRecordZone` — Logical grouping
|
||||
- `CKAsset` — Binary file storage
|
||||
|
||||
### Basic Operations
|
||||
|
||||
```swift
|
||||
// ✅ Container and database
|
||||
let container = CKContainer.default()
|
||||
let privateDatabase = container.privateCloudDatabase
|
||||
let publicDatabase = container.publicCloudDatabase
|
||||
|
||||
// ✅ Create record
|
||||
let record = CKRecord(recordType: "Task")
|
||||
record["title"] = "Buy groceries"
|
||||
record["isCompleted"] = false
|
||||
record["dueDate"] = Date()
|
||||
|
||||
// ✅ Save record
|
||||
try await privateDatabase.save(record)
|
||||
|
||||
// ✅ Fetch record
|
||||
let recordID = CKRecord.ID(recordName: "task-123")
|
||||
let fetchedRecord = try await privateDatabase.record(for: recordID)
|
||||
|
||||
// ✅ Query records
|
||||
let predicate = NSPredicate(format: "isCompleted == NO")
|
||||
let query = CKQuery(recordType: "Task", predicate: predicate)
|
||||
let (matchResults, _) = try await privateDatabase.records(matching: query)
|
||||
|
||||
for result in matchResults {
|
||||
if case .success(let record) = result.1 {
|
||||
print("Task: \(record["title"] as? String ?? "")")
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Delete record
|
||||
try await privateDatabase.deleteRecord(withID: recordID)
|
||||
```
|
||||
|
||||
### Update Record
|
||||
|
||||
```swift
|
||||
// ✅ Fetch-then-modify-then-save (prevents serverRecordChanged errors)
|
||||
let record = try await privateDatabase.record(for: recordID)
|
||||
record["title"] = "Updated title"
|
||||
record["isCompleted"] = true
|
||||
try await privateDatabase.save(record)
|
||||
|
||||
// ✅ Batch modify (save + delete in one operation)
|
||||
let operation = CKModifyRecordsOperation(
|
||||
recordsToSave: [updatedRecord1, updatedRecord2],
|
||||
recordIDsToDelete: [deletedID]
|
||||
)
|
||||
operation.perRecordSaveBlock = { recordID, result in
|
||||
switch result {
|
||||
case .success: print("Saved: \(recordID)")
|
||||
case .failure(let error): print("Failed: \(recordID) — \(error)")
|
||||
}
|
||||
}
|
||||
try await privateDatabase.add(operation)
|
||||
```
|
||||
|
||||
### Conflict Resolution
|
||||
|
||||
```swift
|
||||
// ✅ Handle conflicts with savePolicy
|
||||
let operation = CKModifyRecordsOperation(
|
||||
recordsToSave: [record],
|
||||
recordIDsToDelete: nil
|
||||
)
|
||||
|
||||
// Save only if server version unchanged
|
||||
operation.savePolicy = .ifServerRecordUnchanged
|
||||
|
||||
// OR: Always overwrite server
|
||||
operation.savePolicy = .changedKeys // Only changed fields
|
||||
|
||||
operation.modifyRecordsResultBlock = { result in
|
||||
switch result {
|
||||
case .success:
|
||||
print("Saved")
|
||||
case .failure(let error as CKError):
|
||||
if error.code == .serverRecordChanged {
|
||||
// Conflict - merge manually
|
||||
let serverRecord = error.serverRecord
|
||||
let clientRecord = error.clientRecord
|
||||
let merged = mergeRecords(server: serverRecord, client: clientRecord)
|
||||
// Retry with merged record
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
privateDatabase.add(operation)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Scopes
|
||||
|
||||
| Scope | Accessibility | SwiftData Support | Use Case |
|
||||
|-------|---------------|-------------------|----------|
|
||||
| **Private** | User only | ✅ Yes | Personal user data |
|
||||
| **Public** | All users | ❌ No | Shared/public content |
|
||||
| **Shared** | Invited users | ❌ No | Collaboration |
|
||||
|
||||
### Private Database
|
||||
|
||||
```swift
|
||||
// ✅ Private database (most common)
|
||||
let privateDB = CKContainer.default().privateCloudDatabase
|
||||
|
||||
// User must be signed into iCloud
|
||||
// Data syncs across user's devices
|
||||
// Not visible to other users
|
||||
```
|
||||
|
||||
### Public Database
|
||||
|
||||
```swift
|
||||
// ✅ Public database (for shared content)
|
||||
let publicDB = CKContainer.default().publicCloudDatabase
|
||||
|
||||
// Accessible to all app users
|
||||
// Even unauthenticated users can read
|
||||
// Writes require authentication
|
||||
// Use for: Leaderboards, public content, discovery
|
||||
```
|
||||
|
||||
### Shared Database
|
||||
|
||||
```swift
|
||||
// ✅ Shared database (collaboration)
|
||||
let sharedDB = CKContainer.default().sharedCloudDatabase
|
||||
|
||||
// For CKShare-based collaboration
|
||||
// Users invited to specific record zones
|
||||
// Use for: Shared documents, team data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CloudKit Assets (Files)
|
||||
|
||||
```swift
|
||||
// ✅ Store files as CKAsset
|
||||
let imageURL = saveImageToTempFile(image) // Must be file URL
|
||||
let asset = CKAsset(fileURL: imageURL)
|
||||
|
||||
let record = CKRecord(recordType: "Photo")
|
||||
record["image"] = asset
|
||||
record["caption"] = "Sunset"
|
||||
|
||||
try await privateDatabase.save(record)
|
||||
|
||||
// ✅ Retrieve asset
|
||||
let fetchedRecord = try await privateDatabase.record(for: recordID)
|
||||
if let asset = fetchedRecord["image"] as? CKAsset,
|
||||
let fileURL = asset.fileURL {
|
||||
let imageData = try Data(contentsOf: fileURL)
|
||||
let image = UIImage(data: imageData)
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: CKAsset requires a **file URL**, not Data. Write data to temp file first.
|
||||
|
||||
---
|
||||
|
||||
## CloudKit Console (Monitoring - WWDC 2024)
|
||||
|
||||
### Developer Notifications
|
||||
|
||||
Set up alerts for:
|
||||
- Schema changes
|
||||
- Quota exceeded
|
||||
- High error rates
|
||||
- Custom thresholds
|
||||
|
||||
### Telemetry
|
||||
|
||||
Monitor:
|
||||
- Request count
|
||||
- Error rate
|
||||
- Latency (p50, p95, p99)
|
||||
- Bandwidth usage
|
||||
|
||||
### Logs
|
||||
|
||||
View:
|
||||
- Individual requests
|
||||
- Error details
|
||||
- Performance bottlenecks
|
||||
|
||||
**Access**: https://icloud.developer.apple.com/dashboard
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Initial Sync
|
||||
|
||||
```swift
|
||||
// ✅ Fetch all records on first launch
|
||||
func performInitialSync() async throws {
|
||||
let predicate = NSPredicate(value: true) // All records
|
||||
let query = CKQuery(recordType: "Task", predicate: predicate)
|
||||
|
||||
let (results, _) = try await privateDatabase.records(matching: query)
|
||||
|
||||
for result in results {
|
||||
if case .success(let record) = result.1 {
|
||||
saveToLocalDatabase(record)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Incremental Sync
|
||||
|
||||
```swift
|
||||
// ✅ Use CKServerChangeToken for incremental fetches
|
||||
func fetchChanges(since token: CKServerChangeToken?) async throws {
|
||||
let zoneID = CKRecordZone.ID(zoneName: "Tasks")
|
||||
|
||||
let config = CKFetchRecordZoneChangesOperation.ZoneConfiguration(
|
||||
previousServerChangeToken: token
|
||||
)
|
||||
|
||||
let operation = CKFetchRecordZoneChangesOperation(
|
||||
recordZoneIDs: [zoneID],
|
||||
configurationsByRecordZoneID: [zoneID: config]
|
||||
)
|
||||
|
||||
operation.recordWasChangedBlock = { recordID, result in
|
||||
if case .success(let record) = result {
|
||||
updateLocalDatabase(with: record)
|
||||
}
|
||||
}
|
||||
|
||||
operation.recordWithIDWasDeletedBlock = { recordID, _ in
|
||||
deleteFromLocalDatabase(recordID)
|
||||
}
|
||||
|
||||
operation.recordZoneFetchResultBlock = { zoneID, result in
|
||||
if case .success(let (token, _, _)) = result {
|
||||
saveChangeToken(token) // For next fetch
|
||||
}
|
||||
}
|
||||
|
||||
try await privateDatabase.add(operation)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entitlements
|
||||
|
||||
Required entitlements in Xcode:
|
||||
|
||||
```xml
|
||||
<!-- iCloud capability -->
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
|
||||
<!-- CloudKit container -->
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array>
|
||||
<string>iCloud.com.example.app</string>
|
||||
</array>
|
||||
```
|
||||
|
||||
**Setup**:
|
||||
1. Xcode → Target → Signing & Capabilities
|
||||
2. "+ Capability" → iCloud
|
||||
3. Check "CloudKit"
|
||||
4. Select or create container
|
||||
|
||||
---
|
||||
|
||||
## Subscriptions (Push Notifications)
|
||||
|
||||
### Database Subscription
|
||||
|
||||
```swift
|
||||
// ✅ Get notified of ANY change in private database
|
||||
let subscription = CKDatabaseSubscription(subscriptionID: "all-changes")
|
||||
|
||||
let notificationInfo = CKSubscription.NotificationInfo()
|
||||
notificationInfo.shouldSendContentAvailable = true // Silent push
|
||||
subscription.notificationInfo = notificationInfo
|
||||
|
||||
try await privateDatabase.save(subscription)
|
||||
```
|
||||
|
||||
### Query Subscription
|
||||
|
||||
```swift
|
||||
// ✅ Get notified when records matching a query change
|
||||
let predicate = NSPredicate(format: "priority > 3")
|
||||
let subscription = CKQuerySubscription(
|
||||
recordType: "Task",
|
||||
predicate: predicate,
|
||||
subscriptionID: "high-priority-tasks",
|
||||
options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
|
||||
)
|
||||
|
||||
let notificationInfo = CKSubscription.NotificationInfo()
|
||||
notificationInfo.alertBody = "High priority task changed"
|
||||
notificationInfo.shouldBadge = true
|
||||
subscription.notificationInfo = notificationInfo
|
||||
|
||||
try await privateDatabase.save(subscription)
|
||||
```
|
||||
|
||||
### Zone Subscription
|
||||
|
||||
```swift
|
||||
// ✅ Get notified of any change in a specific zone
|
||||
let zoneID = CKRecordZone.ID(zoneName: "Tasks")
|
||||
let subscription = CKRecordZoneSubscription(
|
||||
zoneID: zoneID,
|
||||
subscriptionID: "tasks-zone"
|
||||
)
|
||||
|
||||
let notificationInfo = CKSubscription.NotificationInfo()
|
||||
notificationInfo.shouldSendContentAvailable = true
|
||||
subscription.notificationInfo = notificationInfo
|
||||
|
||||
try await privateDatabase.save(subscription)
|
||||
```
|
||||
|
||||
### Handling Push Notifications
|
||||
|
||||
```swift
|
||||
// In AppDelegate
|
||||
func application(_ application: UIApplication,
|
||||
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
|
||||
let notification = CKNotification(fromRemoteNotificationDictionary: userInfo)
|
||||
|
||||
if notification.subscriptionID == "all-changes" {
|
||||
try? await fetchChanges(since: savedChangeToken)
|
||||
return .newData
|
||||
}
|
||||
return .noData
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sharing Records
|
||||
|
||||
### Create a Share
|
||||
|
||||
```swift
|
||||
// ✅ Share a record with other users
|
||||
let record = try await privateDatabase.record(for: recordID)
|
||||
|
||||
// Record must be in a custom zone (not default zone)
|
||||
let share = CKShare(rootRecord: record)
|
||||
share[CKShare.SystemFieldKey.title] = "Shared Task List"
|
||||
share.publicPermission = .none // Invite-only
|
||||
|
||||
// Save both the record and share together
|
||||
let operation = CKModifyRecordsOperation(
|
||||
recordsToSave: [record, share],
|
||||
recordIDsToDelete: nil
|
||||
)
|
||||
try await privateDatabase.add(operation)
|
||||
```
|
||||
|
||||
### Present Sharing UI
|
||||
|
||||
```swift
|
||||
import CloudKit
|
||||
import UIKit
|
||||
|
||||
// ✅ UIKit sharing controller
|
||||
let sharingController = UICloudSharingController(share: share, container: container)
|
||||
sharingController.delegate = self
|
||||
present(sharingController, animated: true)
|
||||
|
||||
// Delegate methods
|
||||
extension ViewController: UICloudSharingControllerDelegate {
|
||||
func cloudSharingController(_ csc: UICloudSharingController,
|
||||
failedToSaveShareWithError error: Error) {
|
||||
print("Share failed: \(error)")
|
||||
}
|
||||
|
||||
func itemTitle(for csc: UICloudSharingController) -> String? {
|
||||
return "My Shared List"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Manage Participants
|
||||
|
||||
```swift
|
||||
// ✅ Check participants
|
||||
for participant in share.participants {
|
||||
print("\(participant.userIdentity.nameComponents?.givenName ?? "Unknown")")
|
||||
print(" Acceptance: \(participant.acceptanceStatus)")
|
||||
print(" Permission: \(participant.permission)")
|
||||
// .readOnly, .readWrite, .none
|
||||
}
|
||||
|
||||
// ✅ Remove participant
|
||||
share.removeParticipant(participant)
|
||||
try await privateDatabase.save(share)
|
||||
```
|
||||
|
||||
### Accept a Share
|
||||
|
||||
```swift
|
||||
// In SceneDelegate or AppDelegate
|
||||
func userDidAcceptCloudKitShareWith(_ cloudKitShareMetadata: CKShare.Metadata) {
|
||||
let operation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
|
||||
operation.acceptSharesResultBlock = { result in
|
||||
switch result {
|
||||
case .success: print("Share accepted")
|
||||
case .failure(let error): print("Accept failed: \(error)")
|
||||
}
|
||||
}
|
||||
CKContainer(identifier: cloudKitShareMetadata.containerIdentifier)
|
||||
.add(operation)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Modern API (iOS 17+) | Legacy API |
|
||||
|------|----------------------|------------|
|
||||
| Structured data sync | SwiftData + CloudKit | CKSyncEngine or CKDatabase |
|
||||
| Custom persistence sync | CKSyncEngine | CKDatabase |
|
||||
| Conflict resolution | Automatic (SwiftData/CKSyncEngine) | Manual (savePolicy) |
|
||||
| Account changes | Handled automatically | Manual detection |
|
||||
| Monitoring | CloudKit Console telemetry | Manual logging |
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `axiom-swiftdata` — SwiftData implementation details
|
||||
- `axiom-storage` — Choose CloudKit vs iCloud Drive
|
||||
- `axiom-icloud-drive-ref` — File-based iCloud sync
|
||||
- `axiom-cloud-sync-diag` — Debug CloudKit sync issues
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-12
|
||||
**Skill Type**: Reference
|
||||
**Minimum iOS**: 10.0 (basic), 17.0 (CKSyncEngine, SwiftData integration)
|
||||
**WWDC Sessions**: 2023-10188 (CKSyncEngine), 2024-10122 (CloudKit Console)
|
||||
3
.claude/skills/axiom-cloudkit-ref/agents/openai.yaml
Normal file
3
.claude/skills/axiom-cloudkit-ref/agents/openai.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "CloudKit Reference"
|
||||
short_description: "Implementing 'CloudKit sync', 'CKSyncEngine', 'CKRecord', 'CKDatabase', 'SwiftData CloudKit', 'shared database', 'pub..."
|
||||
Reference in New Issue
Block a user