Tüm Yazılar
KategoriBackend
Okuma Süresi
24 dk
Yayın Tarihi
...
Kelime Sayısı
3.232kelime

Kahveni hazırla - bu içerikli bir makale!

CloudKit ile veri senkronizasyonu. Public/private database, subscriptions, conflict resolution ve offline support.

CloudKit ile iCloud Senkronizasyonu

# 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


🎯 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 > iCloud
2// ✅ CloudKit seç
3// ✅ Container oluştur: iCloud.com.mycompany.myapp
4 
5import CloudKit
6 
7// MARK: - CloudKit Configuration
8enum 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.privateCloudDatabase
17 }
18
19 static var publicDatabase: CKDatabase {
20 container.publicCloudDatabase
21 }
22
23 static var sharedDatabase: CKDatabase {
24 container.sharedCloudDatabase
25 }
26
27 // Custom zone for better sync
28 static let customZone = CKRecordZone(zoneName: "MyAppZone")
29 static var customZoneID: CKRecordZone.ID { customZone.zoneID }
30}

2. Account Status Check

swift
1// MARK: - CloudKit Manager
2@MainActor
3final class CloudKitManager: ObservableObject {
4 static let shared = CloudKitManager()
5
6 @Published private(set) var accountStatus: CKAccountStatus = .couldNotDetermine
7 @Published private(set) var isAvailable = false
8 @Published private(set) var userName: String?
9 @Published private(set) var error: CloudKitError?
10
11 private let container = CloudKitConfig.container
12 private let privateDB = CloudKitConfig.privateDatabase
13
14 private init() {
15 Task {
16 await checkAccountStatus()
17 await setupCustomZone()
18 }
19
20 // Account değişikliklerini dinle
21 NotificationCenter.default.addObserver(
22 forName: .CKAccountChanged,
23 object: nil,
24 queue: .main
25 ) { [weak self] _ in
26 Task { await self?.checkAccountStatus() }
27 }
28 }
29
30 // MARK: - Account Status
31 func checkAccountStatus() async {
32 do {
33 accountStatus = try await container.accountStatus()
34 isAvailable = accountStatus == .available
35
36 if isAvailable {
37 // Kullanıcı bilgisi al
38 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 = false
46 }
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 firstName
54 }
55 return "User"
56 }
57
58 // MARK: - Custom Zone Setup
59 func setupCustomZone() async {
60 guard isAvailable else { return }
61
62 do {
63 // Zone var mı kontrol et, yoksa oluştur
64 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 Types
77enum CloudKitError: LocalizedError {
78 case accountError(Error)
79 case networkError
80 case quotaExceeded
81 case recordNotFound
82 case conflictError(serverRecord: CKRecord)
83 case permissionError
84 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.localizedDescription
102 }
103 }
104}

📦 Model ve Record Mapping

3. CloudKit Protocol

swift
1import CloudKit
2 
3// MARK: - CloudKit Record Protocol
4protocol 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() -> CKRecord
14}
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 existing
25 }
26 return CKRecord.ID(
27 recordName: id.uuidString,
28 zoneID: CloudKitConfig.customZoneID
29 )
30 }
31}

4. Model Implementation

swift
1// MARK: - Note Model
2struct Note: CloudKitRecord, Equatable, Hashable {
3 let id: UUID
4 var ckRecordID: CKRecord.ID?
5 var title: String
6 var content: String
7 var color: NoteColor
8 var isPinned: Bool
9 var tags: [String]
10 let createdAt: Date
11 var modifiedAt: Date
12
13 // Local-only properties (sync edilmez)
14 var isSelected: Bool = false
15
16 enum NoteColor: String, CaseIterable, Codable {
17 case yellow, blue, green, red, purple
18 }
19
20 // MARK: - Init
21 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 = id
30 self.title = title
31 self.content = content
32 self.color = color
33 self.isPinned = isPinned
34 self.tags = tags
35 self.createdAt = Date()
36 self.modifiedAt = Date()
37 }
38
39 // MARK: - From CKRecord
40 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 nil
50 }
51
52 self.id = id
53 self.ckRecordID = record.recordID
54 self.title = title
55 self.content = content
56 self.color = color
57 self.isPinned = record["isPinned"] as? Bool ?? false
58 self.tags = record["tags"] as? [String] ?? []
59 self.createdAt = createdAt
60 self.modifiedAt = modifiedAt
61 }
62
63 // MARK: - To CKRecord
64 func toRecord() -> CKRecord {
65 let recordID = makeRecordID()
66 let record = CKRecord(recordType: Self.recordType, recordID: recordID)
67
68 record["uuid"] = id.uuidString
69 record["title"] = title
70 record["content"] = content
71 record["color"] = color.rawValue
72 record["isPinned"] = isPinned
73 record["tags"] = tags
74 record["createdAt"] = createdAt
75 record["modifiedAt"] = modifiedAt
76
77 return record
78 }
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 = .purple
87 title = "🎮 Konami Code Activated!"
88 }
89 }
90}

🔄 CRUD Operations

5. Save, Fetch, Delete

swift
1// MARK: - CloudKit Operations
2extension CloudKitManager {
3
4 // MARK: - Save
5 func save<T: CloudKitRecord>(_ item: T) async throws -> T {
6 guard isAvailable else {
7 throw CloudKitError.accountError(NSError())
8 }
9
10 var mutableItem = item
11 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üncelle
19 if var updated = T(from: savedRecord) {
20 updated.ckRecordID = savedRecord.recordID
21 return updated
22 }
23 return mutableItem
24
25 } catch let error as CKError {
26 throw handleCKError(error)
27 }
28 }
29
30 // MARK: - Fetch All
31 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 = 100
38 ) async throws -> [T] {
39
40 let query = CKQuery(recordType: T.recordType, predicate: predicate)
41 query.sortDescriptors = sortDescriptors
42
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: limit
51 )
52
53 let items = records.compactMap { T(from: $0) }
54 allRecords.append(contentsOf: items)
55 cursor = newCursor
56
57 } while cursor != nil
58
59 return allRecords
60 }
61
62 private func fetchBatch(
63 query: CKQuery,
64 cursor: CKQueryOperation.Cursor?,
65 limit: Int
66 ) async throws -> ([CKRecord], CKQueryOperation.Cursor?) {
67
68 if let cursor {
69 // Continue from cursor
70 let (results, newCursor) = try await privateDB.records(
71 continuingMatchFrom: cursor,
72 resultsLimit: limit
73 )
74 let records = results.compactMap { try? $0.1.get() }
75 return (records, newCursor)
76 } else {
77 // Initial query
78 let (results, newCursor) = try await privateDB.records(
79 matching: query,
80 inZoneWith: CloudKitConfig.customZoneID,
81 resultsLimit: limit
82 )
83 let records = results.compactMap { try? $0.1.get() }
84 return (records, newCursor)
85 }
86 }
87
88 // MARK: - Fetch Single
89 func fetch<T: CloudKitRecord>(
90 _ type: T.Type,
91 id: UUID
92 ) async throws -> T? {
93
94 let recordID = CKRecord.ID(
95 recordName: id.uuidString,
96 zoneID: CloudKitConfig.customZoneID
97 )
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 nil
104 }
105 }
106
107 // MARK: - Delete
108 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 Save
114 func saveAll<T: CloudKitRecord>(_ items: [T]) async throws -> [T] {
115 guard !items.isEmpty else { return [] }
116
117 let records = items.map { item -> CKRecord in
118 var mutable = item
119 mutable.modifiedAt = Date()
120 return mutable.toRecord()
121 }
122
123 let operation = CKModifyRecordsOperation(
124 recordsToSave: records,
125 recordIDsToDelete: nil
126 )
127 operation.savePolicy = .changedKeys // Sadece değişen field'ları gönder
128
129 let (savedRecords, _) = try await privateDB.modifyRecords(
130 saving: records,
131 deleting: [],
132 savePolicy: .changedKeys,
133 atomically: true
134 )
135
136 return savedRecords.compactMap { result -> T? in
137 guard case .success(let record) = result.1 else { return nil }
138 return T(from: record)
139 }
140 }
141
142 // MARK: - Error Handling
143 private func handleCKError(_ error: CKError) -> CloudKitError {
144 switch error.code {
145 case .networkFailure, .networkUnavailable:
146 return .networkError
147 case .quotaExceeded:
148 return .quotaExceeded
149 case .unknownItem:
150 return .recordNotFound
151 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 .permissionError
158 default:
159 return .unknownError(error)
160 }
161 }
162}

📡 Real-Time Sync ve Subscriptions

6. Change Subscriptions

swift
1// MARK: - Subscriptions
2extension CloudKitManager {
3
4 /// Database değişikliklerini dinle
5 func setupSubscriptions() async throws {
6 // Önceki subscription'ları temizle
7 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 push
17 // notificationInfo.alertBody = "Notların güncellendi!" // Görünür push
18 subscription.notificationInfo = notificationInfo
19
20 try await privateDB.save(subscription)
21 print("✅ Subscription created")
22 }
23
24 /// Push notification ile değişiklik bildirimi
25 func handlePushNotification(userInfo: [AnyHashable: Any]) async {
26 guard let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) else {
27 return
28 }
29
30 // Silent push için sync başlat
31 if notification.subscriptionID == "private-changes" {
32 await fetchChanges()
33 }
34 }
35}
36 
37// MARK: - AppDelegate Push Handling
38extension AppDelegate {
39 func application(
40 _ application: UIApplication,
41 didReceiveRemoteNotification userInfo: [AnyHashable: Any],
42 fetchCompletionHandler completionHandler: @escaping(UIBackgroundFetchResult) -> Void
43 ) {
44 Task {
45 await CloudKitManager.shared.handlePushNotification(userInfo: userInfo)
46 completionHandler(.newData)
47 }
48 }
49}

7. Change Token Sync

swift
1// MARK: - Incremental Sync
2extension 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ükle
11 let previousToken = loadChangeToken()
12
13 // Zone changes al
14 let config = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
15 config.previousServerChangeToken = previousToken
16
17 var changedRecords: [CKRecord] = []
18 var deletedRecordIDs: [CKRecord.ID] = []
19 var newToken: CKServerChangeToken?
20
21 // Modern async API
22 let changes = privateDB.recordZoneChanges(
23 inZoneWith: CloudKitConfig.customZoneID,
24 since: previousToken
25 )
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'ı kaydet
40 // Not: iOS 17+ token changes response'da
41 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": deletedRecordIDs
53 ]
54 )
55 }
56
57 } catch {
58 print("❌ Fetch changes error: \(error)")
59 }
60 }
61
62 // MARK: - Token Persistence
63 private func loadChangeToken() -> CKServerChangeToken? {
64 guard let data = UserDefaults.standard.data(forKey: changeTokenKey) else {
65 return nil
66 }
67 return try? NSKeyedUnarchiver.unarchivedObject(
68 ofClass: CKServerChangeToken.self,
69 from: data
70 )
71 }
72
73 private func saveChangeToken(_ token: CKServerChangeToken) {
74 guard let data = try? NSKeyedArchiver.archivedData(
75 withRootObject: token,
76 requiringSecureCoding: true
77 ) 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 Resolution
2enum ConflictResolutionStrategy {
3 case clientWins // Local değişiklik kazanır
4 case serverWins // Server değişiklik kazanır
5 case lastWriteWins // En son modifiedAt kazanır
6 case merge // Field-level merge
7 case askUser // Kullanıcıya sor
8}
9 
10extension CloudKitManager {
11
12 /// Conflict-aware save
13 func saveWithConflictResolution<T: CloudKitRecord>(
14 _ item: T,
15 strategy: ConflictResolutionStrategy = .lastWriteWins
16 ) 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 çöz
23 let resolvedRecord = resolveConflict(
24 local: item.toRecord(),
25 server: serverRecord,
26 strategy: strategy
27 )
28
29 // Çözülmüş record'u kaydet
30 let savedRecord = try await privateDB.save(resolvedRecord)
31
32 if let resolved = T(from: savedRecord) {
33 return resolved
34 }
35 throw CloudKitError.unknownError(NSError())
36 }
37 }
38
39 private func resolveConflict(
40 local: CKRecord,
41 server: CKRecord,
42 strategy: ConflictResolutionStrategy
43 ) -> CKRecord {
44
45 switch strategy {
46 case .clientWins:
47 // Local değerleri server record'a kopyala
48 for key in local.allKeys() {
49 server[key] = local[key]
50 }
51 return server
52
53 case .serverWins:
54 // Server olduğu gibi kalsın
55 return server
56
57 case .lastWriteWins:
58 // modifiedAt karşılaştır
59 let localDate = local["modifiedAt"] as? Date ?? .distantPast
60 let serverDate = server["modifiedAt"] as? Date ?? .distantPast
61
62 if localDate > serverDate {
63 for key in local.allKeys() {
64 server[key] = local[key]
65 }
66 }
67 return server
68
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 etsin
75 return server
76 }
77 }
78
79 private func mergeRecords(local: CKRecord, server: CKRecord) -> CKRecord {
80 // Field-specific merge logic
81 // Örnek: title her zaman local, content her zaman server
82
83 let allKeys = Set(local.allKeys() + server.allKeys())
84
85 for key in allKeys {
86 // Null olmayan değeri tercih et
87 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 server
96 }
97}

💾 Offline-First Architecture

9. Local Cache + Sync

swift
1import SwiftData
2 
3// MARK: - Offline-First Note Store
4@MainActor
5final class NoteStore: ObservableObject {
6 @Published private(set) var notes: [Note] = []
7 @Published private(set) var syncStatus: SyncStatus = .idle
8 @Published private(set) var pendingChanges: Int = 0
9
10 private let cloudKit = CloudKitManager.shared
11 private var modelContext: ModelContext?
12
13 enum SyncStatus {
14 case idle
15 case syncing
16 case error(Error)
17 case success
18 }
19
20 init() {
21 // SwiftData setup (veya Core Data)
22 setupLocalStorage()
23
24 // CloudKit değişikliklerini dinle
25 NotificationCenter.default.addObserver(
26 forName: .cloudKitDidFetchChanges,
27 object: nil,
28 queue: .main
29 ) { [weak self] notification in
30 self?.handleRemoteChanges(notification)
31 }
32
33 // İlk yükleme
34 Task {
35 await loadFromLocal()
36 await sync()
37 }
38 }
39
40 // MARK: - Local Storage
41 private func setupLocalStorage() {
42 // SwiftData veya Core Data setup
43 }
44
45 func loadFromLocal() async {
46 // Local cache'den yükle
47 }
48
49 // MARK: - CRUD (Offline-First)
50 func add(_ note: Note) async {
51 // 1. Önce local'e kaydet
52 notes.append(note)
53 saveToLocal(note, action: .create)
54
55 // 2. Background'da CloudKit'e sync et
56 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üncelle
69 if let index = notes.firstIndex(where: { $0.id == note.id }) {
70 notes[index] = note
71 }
72 saveToLocal(note, action: .update)
73
74 // 2. CloudKit sync
75 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 sil
87 notes.removeAll { $0.id == note.id }
88 saveToLocal(note, action: .delete)
89
90 // 2. CloudKit'ten sil
91 Task {
92 do {
93 try await cloudKit.delete(note)
94 } catch {
95 // Zaten silinmişse OK
96 }
97 }
98 }
99
100 // MARK: - Sync
101 func sync() async {
102 guard cloudKit.isAvailable else {
103 syncStatus = .error(CloudKitError.accountError(NSError()))
104 return
105 }
106
107 syncStatus = .syncing
108
109 do {
110 // 1. Pending local değişiklikleri gönder
111 await uploadPendingChanges()
112
113 // 2. Remote değişiklikleri al
114 await cloudKit.fetchChanges()
115
116 syncStatus = .success
117 pendingChanges = 0
118
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 et
126 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 later
134 }
135 }
136 }
137
138 // MARK: - Remote Changes Handler
139 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 return
144 }
145
146 // Changed records
147 for record in changed {
148 if let note = Note(from: record) {
149 // Local'de varsa güncelle, yoksa ekle
150 if let index = notes.firstIndex(where: { $0.id == note.id }) {
151 notes[index] = note
152 } else {
153 notes.append(note)
154 }
155 saveToLocal(note, action: .update)
156 }
157 }
158
159 // Deleted records
160 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: - Helpers
169 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 SHEET
2 
3## Quick Reference
4 
5### Database Types
swift
1 
2### Common Operations
swift
1 
2### Error Handling
swift
1 
2### Performance Tips
3- [ ] Custom zone kullan(delta sync için)
4- [ ] Batch operations(50 record max)
5- [ ] Change tokens ile incremental sync
6- [ ] QualityOfService.userInitiated UI için
7- [ ] Background fetch ile silent sync
8 
9### Debug Commands

Okuyucu Ö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

  1. Custom Zone Kullan - Default zone yerine custom zone ile change tokens daha verimli çalışır.
  1. Batch Operations - Tek tek save yerine modifyRecords ile batch işlem yap.
  1. QoS Ayarla - UI işlemleri için .userInitiated, background sync için .utility.
  1. Retry Logic - Network hataları için exponential backoff uygula.
  1. Asset Optimization - Büyük dosyaları CKAsset olarak kaydet.
  1. Reference Kullan - İlişkili veriler için CKRecord.Reference.
  1. Index Oluştur - CloudKit Console'da sık sorgulanan field'ları indexle.
  1. 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.

Etiketler

#CloudKit#iCloud#iOS#Sync#Backend#SwiftData
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz