# CloudKit ile iCloud Senkronizasyonu: Enterprise-Grade Sync 🌩️
Merhaba! Bugün Apple'ın sunduğu en güçlü backend çözümü olan CloudKit'i derinlemesine inceleyeceğiz. Bu rehber sonunda, kullanıcı verilerini tüm cihazlar arasında kusursuz senkronize eden bir sistem kurabileceksin.
İçindekiler
- CloudKit Nedir?
- CloudKit Kurulumu
- Model ve Record Mapping
- CRUD Operations
- Real-Time Sync ve Subscriptions
- Conflict Resolution
- Offline-First Architecture
- Sürpriz Hediye: CloudKit Cheat Sheet
- Pro Tips
- Sonuç
🎯 Bu Yazıda Öğreneceklerin
- CloudKit mimarisi ve database türleri
- CKRecord ve custom model mapping
- Real-time sync ve subscriptions
- Conflict resolution stratejileri
- Offline-first yaklaşım
- Performance optimization
- SwiftData + CloudKit entegrasyonu
📚 CloudKit Nedir?
CloudKit, Apple'ın iCloud altyapısını kullanarak veri depolama ve senkronizasyon sağlayan ücretsiz framework'üdür.
Database Türleri
Tür | Erişim | Kota | Kullanım |
|---|---|---|---|
Private | Sadece kullanıcı | Kullanıcı iCloud kotası | Kişisel veriler |
Public | Herkes okuyabilir | Geliştirici kotası | Paylaşılan içerik |
Shared | Seçili kullanıcılar | Paylaşılan kota | Collaborative features |
💡 Altın İpucu: Private database kullanıcı iCloud kotasını kullanır - SENİN maliyetin yok! Public database'de ise Apple yılda 10GB ücretsiz veriyor.
Dış Kaynaklar:
🏗️ CloudKit Kurulumu
1. Proje Konfigürasyonu
swift
1// Xcode > Signing & Capabilities > + Capability > iCloud2// ✅ CloudKit seç3// ✅ Container oluştur: iCloud.com.mycompany.myapp4 5import CloudKit6 7// MARK: - CloudKit Configuration8enum CloudKitConfig {9 static let containerIdentifier = "iCloud.com.mycompany.myapp"10 11 static var container: CKContainer {12 CKContainer(identifier: containerIdentifier)13 }14 15 static var privateDatabase: CKDatabase {16 container.privateCloudDatabase17 }18 19 static var publicDatabase: CKDatabase {20 container.publicCloudDatabase21 }22 23 static var sharedDatabase: CKDatabase {24 container.sharedCloudDatabase25 }26 27 // Custom zone for better sync28 static let customZone = CKRecordZone(zoneName: "MyAppZone")29 static var customZoneID: CKRecordZone.ID { customZone.zoneID }30}2. Account Status Check
swift
1// MARK: - CloudKit Manager2@MainActor3final class CloudKitManager: ObservableObject {4 static let shared = CloudKitManager()5 6 @Published private(set) var accountStatus: CKAccountStatus = .couldNotDetermine7 @Published private(set) var isAvailable = false8 @Published private(set) var userName: String?9 @Published private(set) var error: CloudKitError?10 11 private let container = CloudKitConfig.container12 private let privateDB = CloudKitConfig.privateDatabase13 14 private init() {15 Task {16 await checkAccountStatus()17 await setupCustomZone()18 }19 20 // Account değişikliklerini dinle21 NotificationCenter.default.addObserver(22 forName: .CKAccountChanged,23 object: nil,24 queue: .main25 ) { [weak self] _ in26 Task { await self?.checkAccountStatus() }27 }28 }29 30 // MARK: - Account Status31 func checkAccountStatus() async {32 do {33 accountStatus = try await container.accountStatus()34 isAvailable = accountStatus == .available35 36 if isAvailable {37 // Kullanıcı bilgisi al38 let userID = try await container.userRecordID()39 userName = try? await fetchUserName(userID: userID)40 }41 42 print("☁️ CloudKit status: \(accountStatus)")43 } catch {44 self.error = .accountError(error)45 isAvailable = false46 }47 }48 49 private func fetchUserName(userID: CKRecord.ID) async throws -> String {50 let userRecord = try await privateDB.record(for: userID)51 // Kullanıcı First Name (iOS 15+)52 if let firstName = userRecord["firstName"] as? String {53 return firstName54 }55 return "User"56 }57 58 // MARK: - Custom Zone Setup59 func setupCustomZone() async {60 guard isAvailable else { return }61 62 do {63 // Zone var mı kontrol et, yoksa oluştur64 let zones = try await privateDB.allRecordZones()65 66 if !zones.contains(where: { $0.zoneID == CloudKitConfig.customZoneID }) {67 let _ = try await privateDB.save(CloudKitConfig.customZone)68 print("✅ Custom zone created")69 }70 } catch {71 print("❌ Zone setup error: \(error)")72 }73 }74}75 76// MARK: - Error Types77enum CloudKitError: LocalizedError {78 case accountError(Error)79 case networkError80 case quotaExceeded81 case recordNotFound82 case conflictError(serverRecord: CKRecord)83 case permissionError84 case unknownError(Error)85 86 var errorDescription: String? {87 switch self {88 case .accountError:89 return "iCloud hesabınız aktif değil."90 case .networkError:91 return "İnternet bağlantısı gerekli."92 case .quotaExceeded:93 return "iCloud depolama alanı dolu."94 case .recordNotFound:95 return "Kayıt bulunamadı."96 case .conflictError:97 return "Sync çakışması oluştu."98 case .permissionError:99 return "Erişim izni reddedildi."100 case .unknownError(let error):101 return error.localizedDescription102 }103 }104}📦 Model ve Record Mapping
3. CloudKit Protocol
swift
1import CloudKit2 3// MARK: - CloudKit Record Protocol4protocol CloudKitRecord: Identifiable, Sendable {5 static var recordType: String { get }6 7 var id: UUID { get }8 var ckRecordID: CKRecord.ID? { get set }9 var createdAt: Date { get }10 var modifiedAt: Date { get set }11 12 init?(from record: CKRecord)13 func toRecord() -> CKRecord14}15 16extension CloudKitRecord {17 static var recordType: String {18 String(describing: Self.self)19 }20 21 /// CKRecord.ID oluştur (custom zone'da)22 func makeRecordID() -> CKRecord.ID {23 if let existing = ckRecordID {24 return existing25 }26 return CKRecord.ID(27 recordName: id.uuidString,28 zoneID: CloudKitConfig.customZoneID29 )30 }31}4. Model Implementation
swift
1// MARK: - Note Model2struct Note: CloudKitRecord, Equatable, Hashable {3 let id: UUID4 var ckRecordID: CKRecord.ID?5 var title: String6 var content: String7 var color: NoteColor8 var isPinned: Bool9 var tags: [String]10 let createdAt: Date11 var modifiedAt: Date12 13 // Local-only properties (sync edilmez)14 var isSelected: Bool = false15 16 enum NoteColor: String, CaseIterable, Codable {17 case yellow, blue, green, red, purple18 }19 20 // MARK: - Init21 init(22 id: UUID = UUID(),23 title: String,24 content: String,25 color: NoteColor = .yellow,26 isPinned: Bool = false,27 tags: [String] = []28 ) {29 self.id = id30 self.title = title31 self.content = content32 self.color = color33 self.isPinned = isPinned34 self.tags = tags35 self.createdAt = Date()36 self.modifiedAt = Date()37 }38 39 // MARK: - From CKRecord40 init?(from record: CKRecord) {41 guard let title = record["title"] as? String,42 let content = record["content"] as? String,43 let colorString = record["color"] as? String,44 let color = NoteColor(rawValue: colorString),45 let createdAt = record["createdAt"] as? Date,46 let modifiedAt = record["modifiedAt"] as? Date,47 let idString = record["uuid"] as? String,48 let id = UUID(uuidString: idString) else {49 return nil50 }51 52 self.id = id53 self.ckRecordID = record.recordID54 self.title = title55 self.content = content56 self.color = color57 self.isPinned = record["isPinned"] as? Bool ?? false58 self.tags = record["tags"] as? [String] ?? []59 self.createdAt = createdAt60 self.modifiedAt = modifiedAt61 }62 63 // MARK: - To CKRecord64 func toRecord() -> CKRecord {65 let recordID = makeRecordID()66 let record = CKRecord(recordType: Self.recordType, recordID: recordID)67 68 record["uuid"] = id.uuidString69 record["title"] = title70 record["content"] = content71 record["color"] = color.rawValue72 record["isPinned"] = isPinned73 record["tags"] = tags74 record["createdAt"] = createdAt75 record["modifiedAt"] = modifiedAt76 77 return record78 }79}80 81// 🐣 Easter Egg: Özel not başlığı82// "konami" yazarsan gizli renk açılır!83extension Note {84 mutating func checkEasterEgg() {85 if title.lowercased() == "konami" {86 color = .purple87 title = "🎮 Konami Code Activated!"88 }89 }90}🔄 CRUD Operations
5. Save, Fetch, Delete
swift
1// MARK: - CloudKit Operations2extension CloudKitManager {3 4 // MARK: - Save5 func save<T: CloudKitRecord>(_ item: T) async throws -> T {6 guard isAvailable else {7 throw CloudKitError.accountError(NSError())8 }9 10 var mutableItem = item11 mutableItem.modifiedAt = Date()12 13 let record = mutableItem.toRecord()14 15 do {16 let savedRecord = try await privateDB.save(record)17 18 // CKRecord.ID'yi güncelle19 if var updated = T(from: savedRecord) {20 updated.ckRecordID = savedRecord.recordID21 return updated22 }23 return mutableItem24 25 } catch let error as CKError {26 throw handleCKError(error)27 }28 }29 30 // MARK: - Fetch All31 func fetchAll<T: CloudKitRecord>(32 ofType type: T.Type,33 predicate: NSPredicate = NSPredicate(value: true),34 sortDescriptors: [NSSortDescriptor] = [35 NSSortDescriptor(key: "modifiedAt", ascending: false)36 ],37 limit: Int = 10038 ) async throws -> [T] {39 40 let query = CKQuery(recordType: T.recordType, predicate: predicate)41 query.sortDescriptors = sortDescriptors42 43 var allRecords: [T] = []44 var cursor: CKQueryOperation.Cursor?45 46 repeat {47 let (records, newCursor) = try await fetchBatch(48 query: query,49 cursor: cursor,50 limit: limit51 )52 53 let items = records.compactMap { T(from: $0) }54 allRecords.append(contentsOf: items)55 cursor = newCursor56 57 } while cursor != nil58 59 return allRecords60 }61 62 private func fetchBatch(63 query: CKQuery,64 cursor: CKQueryOperation.Cursor?,65 limit: Int66 ) async throws -> ([CKRecord], CKQueryOperation.Cursor?) {67 68 if let cursor {69 // Continue from cursor70 let (results, newCursor) = try await privateDB.records(71 continuingMatchFrom: cursor,72 resultsLimit: limit73 )74 let records = results.compactMap { try? $0.1.get() }75 return (records, newCursor)76 } else {77 // Initial query78 let (results, newCursor) = try await privateDB.records(79 matching: query,80 inZoneWith: CloudKitConfig.customZoneID,81 resultsLimit: limit82 )83 let records = results.compactMap { try? $0.1.get() }84 return (records, newCursor)85 }86 }87 88 // MARK: - Fetch Single89 func fetch<T: CloudKitRecord>(90 _ type: T.Type,91 id: UUID92 ) async throws -> T? {93 94 let recordID = CKRecord.ID(95 recordName: id.uuidString,96 zoneID: CloudKitConfig.customZoneID97 )98 99 do {100 let record = try await privateDB.record(for: recordID)101 return T(from: record)102 } catch let error as CKError where error.code == .unknownItem {103 return nil104 }105 }106 107 // MARK: - Delete108 func delete<T: CloudKitRecord>(_ item: T) async throws {109 let recordID = item.makeRecordID()110 try await privateDB.deleteRecord(withID: recordID)111 }112 113 // MARK: - Batch Save114 func saveAll<T: CloudKitRecord>(_ items: [T]) async throws -> [T] {115 guard !items.isEmpty else { return [] }116 117 let records = items.map { item -> CKRecord in118 var mutable = item119 mutable.modifiedAt = Date()120 return mutable.toRecord()121 }122 123 let operation = CKModifyRecordsOperation(124 recordsToSave: records,125 recordIDsToDelete: nil126 )127 operation.savePolicy = .changedKeys // Sadece değişen field'ları gönder128 129 let (savedRecords, _) = try await privateDB.modifyRecords(130 saving: records,131 deleting: [],132 savePolicy: .changedKeys,133 atomically: true134 )135 136 return savedRecords.compactMap { result -> T? in137 guard case .success(let record) = result.1 else { return nil }138 return T(from: record)139 }140 }141 142 // MARK: - Error Handling143 private func handleCKError(_ error: CKError) -> CloudKitError {144 switch error.code {145 case .networkFailure, .networkUnavailable:146 return .networkError147 case .quotaExceeded:148 return .quotaExceeded149 case .unknownItem:150 return .recordNotFound151 case .serverRecordChanged:152 if let serverRecord = error.userInfo[CKRecordChangedErrorServerRecordKey] as? CKRecord {153 return .conflictError(serverRecord: serverRecord)154 }155 return .unknownError(error)156 case .notAuthenticated, .permissionFailure:157 return .permissionError158 default:159 return .unknownError(error)160 }161 }162}📡 Real-Time Sync ve Subscriptions
6. Change Subscriptions
swift
1// MARK: - Subscriptions2extension CloudKitManager {3 4 /// Database değişikliklerini dinle5 func setupSubscriptions() async throws {6 // Önceki subscription'ları temizle7 let existingSubscriptions = try await privateDB.allSubscriptions()8 for sub in existingSubscriptions {9 try await privateDB.deleteSubscription(withID: sub.subscriptionID)10 }11 12 // Zone-based subscription (daha verimli)13 let subscription = CKDatabaseSubscription(subscriptionID: "private-changes")14 15 let notificationInfo = CKSubscription.NotificationInfo()16 notificationInfo.shouldSendContentAvailable = true // Silent push17 // notificationInfo.alertBody = "Notların güncellendi!" // Görünür push18 subscription.notificationInfo = notificationInfo19 20 try await privateDB.save(subscription)21 print("✅ Subscription created")22 }23 24 /// Push notification ile değişiklik bildirimi25 func handlePushNotification(userInfo: [AnyHashable: Any]) async {26 guard let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) else {27 return28 }29 30 // Silent push için sync başlat31 if notification.subscriptionID == "private-changes" {32 await fetchChanges()33 }34 }35}36 37// MARK: - AppDelegate Push Handling38extension AppDelegate {39 func application(40 _ application: UIApplication,41 didReceiveRemoteNotification userInfo: [AnyHashable: Any],42 fetchCompletionHandler completionHandler: @escaping(UIBackgroundFetchResult) -> Void43 ) {44 Task {45 await CloudKitManager.shared.handlePushNotification(userInfo: userInfo)46 completionHandler(.newData)47 }48 }49}7. Change Token Sync
swift
1// MARK: - Incremental Sync2extension CloudKitManager {3 private var changeTokenKey: String { "cloudkit.changeToken" }4 5 /// Sadece değişen kayıtları al (incremental sync)6 func fetchChanges() async {7 guard isAvailable else { return }8 9 do {10 // Önceki token'ı yükle11 let previousToken = loadChangeToken()12 13 // Zone changes al14 let config = CKFetchRecordZoneChangesOperation.ZoneConfiguration()15 config.previousServerChangeToken = previousToken16 17 var changedRecords: [CKRecord] = []18 var deletedRecordIDs: [CKRecord.ID] = []19 var newToken: CKServerChangeToken?20 21 // Modern async API22 let changes = privateDB.recordZoneChanges(23 inZoneWith: CloudKitConfig.customZoneID,24 since: previousToken25 )26 27 for try await change in changes {28 switch change {29 case .changed(let record):30 changedRecords.append(record)31 print("📥 Changed: \(record.recordType) - \(record.recordID.recordName)")32 33 case .deleted(let recordID, let recordType):34 deletedRecordIDs.append(recordID)35 print("🗑️ Deleted: \(recordType) - \(recordID.recordName)")36 }37 }38 39 // Token'ı kaydet40 // Not: iOS 17+ token changes response'da41 if let token = newToken {42 saveChangeToken(token)43 }44 45 // UI güncelle (NotificationCenter veya Combine ile)46 await MainActor.run {47 NotificationCenter.default.post(48 name: .cloudKitDidFetchChanges,49 object: nil,50 userInfo: [51 "changed": changedRecords,52 "deleted": deletedRecordIDs53 ]54 )55 }56 57 } catch {58 print("❌ Fetch changes error: \(error)")59 }60 }61 62 // MARK: - Token Persistence63 private func loadChangeToken() -> CKServerChangeToken? {64 guard let data = UserDefaults.standard.data(forKey: changeTokenKey) else {65 return nil66 }67 return try? NSKeyedUnarchiver.unarchivedObject(68 ofClass: CKServerChangeToken.self,69 from: data70 )71 }72 73 private func saveChangeToken(_ token: CKServerChangeToken) {74 guard let data = try? NSKeyedArchiver.archivedData(75 withRootObject: token,76 requiringSecureCoding: true77 ) else { return }78 79 UserDefaults.standard.set(data, forKey: changeTokenKey)80 }81}82 83extension Notification.Name {84 static let cloudKitDidFetchChanges = Notification.Name("cloudKitDidFetchChanges")85}⚔️ Conflict Resolution
8. Merge Strategies
swift
1// MARK: - Conflict Resolution2enum ConflictResolutionStrategy {3 case clientWins // Local değişiklik kazanır4 case serverWins // Server değişiklik kazanır5 case lastWriteWins // En son modifiedAt kazanır6 case merge // Field-level merge7 case askUser // Kullanıcıya sor8}9 10extension CloudKitManager {11 12 /// Conflict-aware save13 func saveWithConflictResolution<T: CloudKitRecord>(14 _ item: T,15 strategy: ConflictResolutionStrategy = .lastWriteWins16 ) async throws -> T {17 18 do {19 return try await save(item)20 21 } catch CloudKitError.conflictError(let serverRecord) {22 // Conflict oluştu - strategy'ye göre çöz23 let resolvedRecord = resolveConflict(24 local: item.toRecord(),25 server: serverRecord,26 strategy: strategy27 )28 29 // Çözülmüş record'u kaydet30 let savedRecord = try await privateDB.save(resolvedRecord)31 32 if let resolved = T(from: savedRecord) {33 return resolved34 }35 throw CloudKitError.unknownError(NSError())36 }37 }38 39 private func resolveConflict(40 local: CKRecord,41 server: CKRecord,42 strategy: ConflictResolutionStrategy43 ) -> CKRecord {44 45 switch strategy {46 case .clientWins:47 // Local değerleri server record'a kopyala48 for key in local.allKeys() {49 server[key] = local[key]50 }51 return server52 53 case .serverWins:54 // Server olduğu gibi kalsın55 return server56 57 case .lastWriteWins:58 // modifiedAt karşılaştır59 let localDate = local["modifiedAt"] as? Date ?? .distantPast60 let serverDate = server["modifiedAt"] as? Date ?? .distantPast61 62 if localDate > serverDate {63 for key in local.allKeys() {64 server[key] = local[key]65 }66 }67 return server68 69 case .merge:70 // Field-level merge (her field için en son değeri al)71 return mergeRecords(local: local, server: server)72 73 case .askUser:74 // Bu durumda throw et, UI handle etsin75 return server76 }77 }78 79 private func mergeRecords(local: CKRecord, server: CKRecord) -> CKRecord {80 // Field-specific merge logic81 // Örnek: title her zaman local, content her zaman server82 83 let allKeys = Set(local.allKeys() + server.allKeys())84 85 for key in allKeys {86 // Null olmayan değeri tercih et87 if local[key] != nil && server[key] == nil {88 server[key] = local[key]89 }90 // Her ikisi de varsa, modifiedAt'e göre seç91 // (Bu basit bir örnek, gerçek dünyada daha karmaşık olabilir)92 }93 94 server["modifiedAt"] = Date()95 return server96 }97}💾 Offline-First Architecture
9. Local Cache + Sync
swift
1import SwiftData2 3// MARK: - Offline-First Note Store4@MainActor5final class NoteStore: ObservableObject {6 @Published private(set) var notes: [Note] = []7 @Published private(set) var syncStatus: SyncStatus = .idle8 @Published private(set) var pendingChanges: Int = 09 10 private let cloudKit = CloudKitManager.shared11 private var modelContext: ModelContext?12 13 enum SyncStatus {14 case idle15 case syncing16 case error(Error)17 case success18 }19 20 init() {21 // SwiftData setup (veya Core Data)22 setupLocalStorage()23 24 // CloudKit değişikliklerini dinle25 NotificationCenter.default.addObserver(26 forName: .cloudKitDidFetchChanges,27 object: nil,28 queue: .main29 ) { [weak self] notification in30 self?.handleRemoteChanges(notification)31 }32 33 // İlk yükleme34 Task {35 await loadFromLocal()36 await sync()37 }38 }39 40 // MARK: - Local Storage41 private func setupLocalStorage() {42 // SwiftData veya Core Data setup43 }44 45 func loadFromLocal() async {46 // Local cache'den yükle47 }48 49 // MARK: - CRUD (Offline-First)50 func add(_ note: Note) async {51 // 1. Önce local'e kaydet52 notes.append(note)53 saveToLocal(note, action: .create)54 55 // 2. Background'da CloudKit'e sync et56 Task {57 do {58 _ = try await cloudKit.save(note)59 markSynced(note.id)60 } catch {61 markPendingSync(note.id)62 incrementPendingChanges()63 }64 }65 }66 67 func update(_ note: Note) async {68 // 1. Local güncelle69 if let index = notes.firstIndex(where: { $0.id == note.id }) {70 notes[index] = note71 }72 saveToLocal(note, action: .update)73 74 // 2. CloudKit sync75 Task {76 do {77 _ = try await cloudKit.saveWithConflictResolution(note)78 markSynced(note.id)79 } catch {80 markPendingSync(note.id)81 }82 }83 }84 85 func delete(_ note: Note) async {86 // 1. Local'den sil87 notes.removeAll { $0.id == note.id }88 saveToLocal(note, action: .delete)89 90 // 2. CloudKit'ten sil91 Task {92 do {93 try await cloudKit.delete(note)94 } catch {95 // Zaten silinmişse OK96 }97 }98 }99 100 // MARK: - Sync101 func sync() async {102 guard cloudKit.isAvailable else {103 syncStatus = .error(CloudKitError.accountError(NSError()))104 return105 }106 107 syncStatus = .syncing108 109 do {110 // 1. Pending local değişiklikleri gönder111 await uploadPendingChanges()112 113 // 2. Remote değişiklikleri al114 await cloudKit.fetchChanges()115 116 syncStatus = .success117 pendingChanges = 0118 119 } catch {120 syncStatus = .error(error)121 }122 }123 124 private func uploadPendingChanges() async {125 // Local'de pending olan değişiklikleri bul ve upload et126 let pending = getPendingNotes()127 128 for note in pending {129 do {130 _ = try await cloudKit.save(note)131 markSynced(note.id)132 } catch {133 // Retry later134 }135 }136 }137 138 // MARK: - Remote Changes Handler139 private func handleRemoteChanges(_ notification: Notification) {140 guard let userInfo = notification.userInfo,141 let changed = userInfo["changed"] as? [CKRecord],142 let deleted = userInfo["deleted"] as? [CKRecord.ID] else {143 return144 }145 146 // Changed records147 for record in changed {148 if let note = Note(from: record) {149 // Local'de varsa güncelle, yoksa ekle150 if let index = notes.firstIndex(where: { $0.id == note.id }) {151 notes[index] = note152 } else {153 notes.append(note)154 }155 saveToLocal(note, action: .update)156 }157 }158 159 // Deleted records160 for recordID in deleted {161 if let uuid = UUID(uuidString: recordID.recordName) {162 notes.removeAll { $0.id == uuid }163 deleteFromLocal(uuid)164 }165 }166 }167 168 // MARK: - Helpers169 private func saveToLocal(_ note: Note, action: SyncAction) { }170 private func deleteFromLocal(_ id: UUID) { }171 private func markSynced(_ id: UUID) { }172 private func markPendingSync(_ id: UUID) { }173 private func incrementPendingChanges() { pendingChanges += 1 }174 private func getPendingNotes() -> [Note] { [] }175 176 enum SyncAction { case create, update, delete }177}markdown
1# 🎁 CLOUDKIT PRODUCTION CHEAT SHEET2 3## Quick Reference4 5### Database Typesswift
1 2### Common Operationsswift
1 2### Error Handlingswift
1 2### Performance Tips3- [ ] Custom zone kullan(delta sync için)4- [ ] Batch operations(50 record max)5- [ ] Change tokens ile incremental sync6- [ ] QualityOfService.userInitiated UI için7- [ ] Background fetch ile silent sync8 9### Debug CommandsOkuyucu Ödülü
container.privateCloudDatabase // User's data container.publicCloudDatabase // Shared data container.sharedCloudDatabase // Collaboration // Save try await database.save(record) // Fetch try await database.record(for: recordID) // Query try await database.records(matching: query) // Delete try await database.deleteRecord(withID: recordID) // Batch try await database.modifyRecords(saving: [], deleting: []) case .networkFailure → Retry with exponential backoff case .quotaExceeded → Inform user case .serverRecordChanged → Conflict resolution case .unknownItem → Record deleted remotely case .notAuthenticated → Request iCloud login
# CloudKit Console
https://icloud.developer.apple.com
# Reset development environment
# CloudKit Console > Schema > Reset Environment
# View logs
# Console.app > Filter: "CloudKit"
swift
1 2## Quotas(Free Tier)3| Resource | Private DB | Public DB |4|----------|------------|-----------|5| Storage | User iCloud | 10GB |6| Requests/sec | 40 | 400 |7| Assets/day | 250MB | 2.5GB |💡 Pro Tips
- Custom Zone Kullan - Default zone yerine custom zone ile change tokens daha verimli çalışır.
- Batch Operations - Tek tek save yerine modifyRecords ile batch işlem yap.
- QoS Ayarla - UI işlemleri için .userInitiated, background sync için .utility.
- Retry Logic - Network hataları için exponential backoff uygula.
- Asset Optimization - Büyük dosyaları CKAsset olarak kaydet.
- Reference Kullan - İlişkili veriler için CKRecord.Reference.
- Index Oluştur - CloudKit Console'da sık sorgulanan field'ları indexle.
- Push Silent - shouldSendContentAvailable ile arka plan sync.
📖 Sonuç
CloudKit, Apple ekosisteminde ücretsiz ve güçlü bir backend çözümü. Private database kullanıcı kotasını kullandığı için maliyetin neredeyse sıfır!
SwiftData + CloudKit entegrasyonu ile artık sync kodu yazmana bile gerek yok - Apple her şeyi hallediyor.
Bir sonraki yazıda görüşmek üzere! ☁️
*İCloud sync güzel ama zor. Takıldığın yerde Twitter'dan sor!*
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.

