A production-ready Swift Package for iCloud + Core Data sync in SwiftUI apps.
Server-wins by default. Save-safe by design. Per-entity conflict resolution.
Apple's NSPersistentCloudKitContainer is powerful but has sharp edges:
- Default merge policy silently drops remote changes
- Disabling sync requires an app restart
- No per-entity conflict resolution
- Save operations crash if called before the store loads
- Force sync has no completion callback
CloudSyncKit wraps all of this into a clean, safe API.
import CloudSyncKit
@main
struct MyApp: App {
@StateObject private var sync = CloudSyncManager(containerName: "MyApp")
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, sync.rawViewContext)
.environmentObject(sync)
}
}
}let sync = CloudSyncManager(configuration: .init(
containerName: "MyApp",
cloudKitContainerIdentifier: "iCloud.com.myteam.myapp",
entityConflictStrategies: [
// SRS data: keep highest streak, most reviews, lowest ease factor
"WordProgress": SRSMergeStrategy(
minFields: ["easeFactor"],
maxFields: ["correctStreak", "totalReviews"]
),
// Settings: whoever edited last wins
"AppSettings": LatestTimestampStrategy(timestampKey: "updatedAt"),
// Everything else: server wins (default)
]
))// In your Settings view
Section("Data & Sync") {
CloudSyncToggle(manager: syncManager) // Toggle + progress
SyncStatusIndicator(manager: syncManager) // Color-coded status
SyncDebugPanel(manager: syncManager) // Force sync, counts, events
}Every save checks that the persistent store is loaded. No more crashes during app startup.
// Option 1: Safe context wrapper
try syncManager.viewContext.safeSave()
// Option 2: Silent save (logs warning on failure)
syncManager.save()
// Option 3: Extension on any context
try context.cloudSyncSafeSave()Actually tears down and rebuilds the persistent store — not just a flag.
try await syncManager.setSyncEnabled(false) // Stops CloudKit immediately
try await syncManager.setSyncEnabled(true) // Starts CloudKit, checks accountMonitors CloudKit events to know when sync actually finishes.
let result = try await syncManager.forceSync()
print("Success: \(result.success), took \(result.duration)s")Different merge strategies for different entities via custom NSMergePolicy subclass.
Built-in strategies:
ServerWinsStrategy— Remote values always win (default)LocalWinsStrategy— Local values always winLatestTimestampStrategy— Most recent edit winsMaxValueStrategy— Keep highest value for specified fieldsSRSMergeStrategy— Min for ease factor, max for streaks/reviews
Implement ConflictResolutionStrategy for custom logic.
Generic dedup that works with any Core Data entity.
let result = try await syncManager.duplicateCleaner.clean(
entityType: KanaProgress.self,
groupBy: \.character,
keepStrategy: .keepHighestValue(forKey: "totalReviews"),
in: syncManager.rawViewContext
)Real-time CloudKit status via @Published properties.
syncManager.syncStatus // .available, .syncing(.importing), .error(.quotaExceeded), etc.
syncManager.lastSyncDate // Optional<Date>
syncManager.recentEvents // [SyncEvent] — last 50 events- iOS 16+ / macOS 13+
- Swift 5.9+
- Core Data model with
usedWithCloudKit="YES"
dependencies: [
.package(url: "https://github.com/castlewong/CloudSyncKit.git", from: "0.1.0")
]CloudSyncKit/
├── Core/
│ ├── CloudSyncManager — Main @ObservableObject entry point
│ ├── CloudSyncConfiguration — All config options
│ ├── SyncStatus — Status enum, events, errors
│ └── StoreCoordinator — Container lifecycle management
├── Context/
│ ├── SafeManagedObjectContext — Save-safe context wrapper
│ └── ContextProvider — View/background context factory
├── Conflict/
│ ├── ConflictResolutionStrategy — Protocol for custom resolution
│ ├── MergeStrategyPresets — Built-in strategies
│ └── EntityConflictRouter — Per-entity NSMergePolicy subclass
├── Sync/
│ ├── SyncEventMonitor — CloudKit event observer
│ ├── SyncToggleController — Full enable/disable with store rebuild
│ └── ForceSyncOperation — Async force sync with completion
├── Dedup/
│ └── DuplicateCleaner — Generic duplicate detection & cleanup
├── Views/
│ ├── CloudSyncToggle — Drop-in settings toggle
│ ├── SyncStatusIndicator — Status badge/label
│ └── SyncDebugPanel — Full debug view
└── Extensions/
├── NSManagedObjectContext+SafeSave
└── NSPersistentStoreDescription+CloudKit
MIT