Section 11/181 menit
11. CloudKit Sync: Strategi dan Conflict Resolution
11. CloudKit Sync: Strategi dan Conflict Resolution
Setup CloudKit dengan SwiftData
swift
// ModelContainer dengan CloudKit sync
let schema = Schema([User.self, Task.self, Note.self])
let modelConfiguration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
cloudKitDatabase: .automatic // Sync ke iCloud otomatis
)
let container = try ModelContainer(
for: schema,
configurations: modelConfiguration
)
// Requirement untuk CloudKit sync:
// 1. Semua property harus optional ATAU punya default value
// 2. Tidak ada @Attribute(.unique) — CloudKit tidak support constraint ini
// 3. App harus punya CloudKit capability di Xcode
// 4. Entitlements: com.apple.developer.icloud-container-identifiers
Constraint: Model CloudKit-Compatible
swift
// ❌ TIDAK kompatibel CloudKit: non-optional tanpa default, unique constraint
@Model
final class CloudTask {
@Attribute(.unique) var remoteID: String // ← CloudKit tidak support
var title: String // ← Non-optional tanpa default
var isCompleted: Bool // ← Non-optional tanpa default
}
// ✓ CloudKit-compatible: semua optional atau punya default
@Model
final class CloudTask {
var remoteID: String? // ← Optional
var title: String = "" // ← Default value
var isCompleted: Bool = false // ← Default value
var createdAt: Date = Date.now // ← Default value
var notes: String? // ← Optional
}
Monitoring Sync Status
swift
import CloudKit
import SwiftUI
class CloudSyncMonitor: ObservableObject {
@Published var syncStatus: SyncStatus = .idle
@Published var lastSyncDate: Date?
@Published var syncError: Error?
enum SyncStatus {
case idle, syncing, success, error
var description: String {
switch self {
case .idle: return "Menunggu..."
case .syncing: return "Sinkronisasi..."
case .success: return "Tersinkron"
case .error: return "Gagal sinkronisasi"
}
}
}
init() {
setupNotifications()
}
private func setupNotifications() {
// Monitor perubahan iCloud account
NotificationCenter.default.addObserver(
self,
selector: #selector(cloudKitAccountChanged),
name: .CKAccountChanged,
object: nil
)
// Monitor import dari CloudKit
NotificationCenter.default.addObserver(
self,
selector: #selector(persistentStoreRemoteChange),
name: .NSPersistentStoreRemoteChange,
object: nil
)
}
@objc private func cloudKitAccountChanged() {
Task { @MainActor in
let status = try? await CKContainer.default().accountStatus()
syncStatus = status == .available ? .idle : .error
}
}
@objc private func persistentStoreRemoteChange() {
Task { @MainActor in
syncStatus = .success
lastSyncDate = Date.now
}
}
func checkCloudKitAvailability() async -> Bool {
do {
let status = try await CKContainer.default().accountStatus()
return status == .available
} catch {
syncError = error
return false
}
}
}
Strategi Conflict Resolution
swift
// SwiftData dengan CloudKit menggunakan "last write wins" secara default
// Untuk conflict resolution yang lebih complex, tracking timestamps diperlukan
@Model
final class SyncableNote {
var content: String = ""
var updatedAt: Date = Date.now // Track kapan diubah
var deviceID: String = UIDevice.current.identifierForVendor?.uuidString ?? ""
// Versi counter untuk optimistic locking
var version: Int = 0
func update(content: String) {
self.content = content
self.updatedAt = Date.now
self.version += 1
}
}
// Saat fetch setelah sync, merge conflict secara manual jika diperlukan
@ModelActor
actor ConflictResolutionActor {
func resolveConflicts(for noteID: PersistentIdentifier) async throws {
guard let note = modelContext.model(for: noteID) as? SyncableNote else { return }
// Fetch semua versi dengan ID yang sama (setelah sync CloudKit)
let descriptor = FetchDescriptor<SyncableNote>(
predicate: #Predicate { $0.version > 0 }
)
let allVersions = try modelContext.fetch(descriptor)
// Keep versi terbaru berdasarkan updatedAt
let latest = allVersions.max(by: { $0.updatedAt < $1.updatedAt })
for version in allVersions where version.persistentModelID != latest?.persistentModelID {
modelContext.delete(version)
}
try modelContext.save()
}
}