Реалізація синхронізації даних через iCloud
iCloud — вбудована платформа синхронізації Apple, доступна без реєстрації сторонніх сервісів. Користувач iOS очікує, що додаток пам'ятатиме його дані після смены телефону та синхронізуватиме між iPhone та iPad. Завдання розробника — вибрати правильний механізм з трьох доступних: NSUbiquitousKeyValueStore, CloudKit та iCloud Documents (через UIDocument).
NSUbiquitousKeyValueStore
Найпростіший варіант — для малих конфігураційних даних. Ліміт 1 МБ на все сховище, 1024 ключа, до 256 КБ на ключ. Синхронізується автоматично, без кода синхронізації.
let store = NSUbiquitousKeyValueStore.default
// Запис
store.set(userId, forKey: "lastUserId")
store.set(["theme": "dark", "fontSize": 16], forKey: "userSettings")
store.synchronize() // запрашує немедленну синхронізацію, не гарантує
// Читання
let theme = store.string(forKey: "userSettings.theme") ?? "light"
// Підписка на зміни з інших пристроїв
NotificationCenter.default.addObserver(
self,
selector: #selector(iCloudDidChange),
name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
object: NSUbiquitousKeyValueStore.default
)
@objc func iCloudDidChange(_ notification: Notification) {
guard let keys = notification.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String]
else { return }
// Оновлюємо локальне стан для змінених ключів
keys.forEach { updateLocalState(forKey: $0) }
}
Для синхронізації налаштувань, малих користувальницьких даних — ідеально. Для прогресу гри, нотаток, файлів — CloudKit.
CloudKit: Public, Private, Shared Database
CloudKit — повнецінна база даних у iCloud. Три типи сховищ:
- Private Database — дані користувача, видні тільки йому. Витрачають iCloud-квоту користувача, не вашу.
- Public Database — дані додатку, доступні всім. Витрачають вашу квоту розробника.
- Shared Database — для функцій «поділитися з користувачем», спільного редагування.
import CloudKit
class CloudKitManager {
let container = CKContainer(identifier: "iCloud.com.company.appname")
var privateDB: CKDatabase { container.privateCloudDatabase }
// Сохранення нотатки
func saveNote(_ note: Note) async throws {
let record = CKRecord(recordType: "Note",
recordID: CKRecord.ID(recordName: note.id))
record["title"] = note.title as CKRecordValue
record["content"] = note.content as CKRecordValue
record["modifiedAt"] = Date() as CKRecordValue
record["isPinned"] = note.isPinned as CKRecordValue
let savedRecord = try await privateDB.save(record)
print("Saved: \(savedRecord.recordID.recordName)")
}
// Завантаження всіх нотаток
func fetchAllNotes() async throws -> [Note] {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Note", predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "modifiedAt", ascending: false)]
let (results, _) = try await privateDB.records(matching: query)
return results.compactMap { (_, result) in
guard let record = try? result.get() else { return nil }
return Note(
id: record.recordID.recordName,
title: record["title"] as? String ?? "",
content: record["content"] as? String ?? "",
isPinned: record["isPinned"] as? Bool ?? false
)
}
}
}
CKSubscription: push-сповіщення при зміні
Коли користувач змінює дані на iPad, iPhone повинен дізнатися про це немедленно. CKQuerySubscription підписується на зміни записів та присилає silent push:
func setupSubscription() async throws {
let predicate = NSPredicate(value: true)
let subscription = CKQuerySubscription(
recordType: "Note",
predicate: predicate,
subscriptionID: "notes-changes",
options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
)
let notificationInfo = CKSubscription.NotificationInfo()
notificationInfo.shouldSendContentAvailable = true // silent push
subscription.notificationInfo = notificationInfo
try await privateDB.save(subscription)
}
// В AppDelegate / UNUserNotificationCenterDelegate
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult {
let notification = CKNotification(fromRemoteNotificationDictionary: userInfo)
if notification?.containerIdentifier == "iCloud.com.company.appname" {
await cloudKitManager.fetchChanges()
return .newData
}
return .noData
}
Silent push будить додаток у фоне (якщо дозволено Background App Refresh) та додаток підтягує зміни.
CKFetchRecordZoneChangesOperation: ефективна дельта-синхронізація
Запрашувати всі записи при кожній синхронізації — неефективно. CKFetchRecordZoneChangesOperation повертає тільки зміни з момента останної синхронізації через serverChangeToken:
func fetchChanges() async throws {
let zone = CKRecordZone(zoneName: "NotesZone")
var config = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
config.previousServerChangeToken = UserDefaults.standard
.data(forKey: "notesZoneChangeToken")
.flatMap { try? NSKeyedUnarchiver.unarchivedObject(ofClass: CKServerChangeToken.self, from: $0) }
let operation = CKFetchRecordZoneChangesOperation(
recordZoneIDs: [zone.zoneID],
configurationsByRecordZoneID: [zone.zoneID: config]
)
operation.recordWasChangedBlock = { _, result in
guard let record = try? result.get() else { return }
Task { await self.localStore.upsert(record) }
}
operation.recordWithIDWasDeletedBlock = { recordID, _ in
Task { await self.localStore.delete(id: recordID.recordName) }
}
operation.recordZoneFetchResultBlock = { _, result in
guard case .success(let info) = result else { return }
// Зберігаємо токен для наступної дельта-синхронізації
if let tokenData = try? NSKeyedArchiver.archivedData(
withRootObject: info.newServerChangeToken, requiringSecureCoding: true) {
UserDefaults.standard.set(tokenData, forKey: "notesZoneChangeToken")
}
}
privateDB.add(operation)
}
Без serverChangeToken завантажуєте все. З токеном — тільки дельту.
Типічні проблеми
CKError.accountTemporarilyUnavailable. Користувач вийшов з iCloud або вимкнув синхронізацію для додатку. Потрібна обробка — не крешитися, а запропонувати входити або працювати тільки локально.
Network quota exceeded. Занадто часті запити до CloudKit. Apple обмежує частоту. Використовуйте subscriptions + delta sync замість polling.
Конфлікти при одночасному редагуванні. CloudKit не вирішує конфлікти автоматично для Custom Zones. При save по існуючому recordID, якщо recordChangeTag не збігається — помилка serverRecordChanged. Потрібен ручний merge.
Реалізація синхронізації через CloudKit з CKSubscription, дельта-синхронізацією та обробкою конфліктів: 2–4 тижні. Вартість рассчитывается індивідуально.







