Зазначимо: коли користувач змінює iPhone, дані додатку повинні з'явитися на новому пристрої автоматично. Інакше — втрата клієнта. Ми інтегруємо синхронізацію iCloud через три механізми: NSUbiquitousKeyValueStore, CloudKit та iCloud Documents (UIDocument). Кожен вирішує своє завдання, але для складної синхронізації з мінімальними затримками найкраще підходить CloudKit. Синхронізація даних через iCloud — це не просто перенесення файлів, а складний процес з дельтами, конфліктами та обмеженнями сховища. Без правильної архітектури користувач втрачає прогрес, налаштування або нотатки при зміні пристрою.
Один із наших проєктів — додаток для нотаток з багатокористувацьким редагуванням. Перша версія використовувала повне вивантаження всіх записів при кожному запуску. Трафік перевищував 10 МБ на користувача на день. Перехід на дельта-синхронізацію з serverChangeToken скоротив передані дані до 1 МБ — зниження на 90%. Користувачі перестали скаржитися на повільне завантаження, а кількість запитів до CloudKit зменшилася в 10 разів.
Коли використовувати NSUbiquitousKeyValueStore, CloudKit або iCloud Documents?
| Критерій | NSUbiquitousKeyValueStore | CloudKit | iCloud Documents |
|---|---|---|---|
| Максимальний об'єм | 1 МБ, 1024 ключа | 10 МБ/користувач (безкоштовно), дод. 1 ГБ за $0.99/міс | Обмеження вільного місця iCloud |
| Тип даних | Налаштування, прості конфігурації | Довільні записи (нотатки, прогрес, списки) | Файли, зображення, документи |
| Синхронізація | Автоматична, без коду | Вимагає підписок і дельта-оновлень | Автоматична через UIDocument |
| Підтримка конфліктів | Останній запис перемагає | Ручний merge для кастомних зон | Версіонування (UIDocument) |
Apple надає 10 МБ безкоштовного сховища CloudKit на користувача, додатковий 1 ГБ коштує $0.99 на місяць. Економія на трафіку від дельта-синхронізації може скоротити витрати на запити в 5 разів.
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 для складної синхронізації?
CloudKit — повноцінна база даних в iCloud. Три типи сховищ:
| Тип бази | Видимість | Витрата квоти | Приклад використання |
|---|---|---|---|
| Private Database | Тільки користувач | Користувацька | Особисті нотатки, налаштування |
| 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
)
}
}
}
Як налаштувати дельта-синхронізацію: покрокова інструкція
-
Створіть підписку на зміни записів (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) } -
Реалізуйте обробку push-сповіщень в AppDelegate — при отриманні silent push викликайте метод fetchChanges().
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 } -
Використовуйте 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) }
Як уникнути конфліктів при одночасному редагуванні?
CloudKit не вирішує конфлікти автоматично для Custom Zones. При save по існуючому recordID, якщо recordChangeTag не збігається — помилка serverRecordChanged. Потрібен ручний merge. Ми використовуємо стратегію last-writer-wins або тристороннє злиття. Ось обробник конфлікту:
// В блоці save з CKModifyRecordsOperation
operation.perRecordSaveBlock = { recordID, saveResult in
if case .failure(let error) = saveResult {
if let ckError = error as? CKError, ckError.code == .serverRecordChanged {
let serverRecord = ckError.userInfo[CKRecordChangedErrorServerRecordKey] as! CKRecord
let clientRecord = ckError.userInfo[CKRecordChangedErrorClientRecordKey] as! CKRecord
// Вирішуємо конфлікт: беремо останню версію
serverRecord["modifiedAt"] = Date()
operation.recordsToSave = [serverRecord]
}
}
}
Типові проблеми при синхронізації CloudKit
- CKError.accountTemporarilyUnavailable: користувач вийшов з iCloud або вимкнув синхронізацію для додатка. Обробляємо — не крэшим, пропонуємо увійти або працювати локально.
- Network quota exceeded: занадто часті запити до CloudKit. Використовуємо subscriptions + delta sync замість polling.
- Конфлікти при одночасному редагуванні. Вирішуємо ручним merge, як описано вище.
Що входить в роботу
| Етап | Тривалість | Результат |
|---|---|---|
| Аналіз вимог і вибір архітектури | 1–2 дні | Технічне завдання, схема даних |
| Проектування бази CloudKit | 1–2 дні | Моделі записів, індекси, підписки |
| Реалізація синхронізації (iOS) | 5–10 днів | Код з інтеграцією CloudKit, обробкою конфліктів |
| Налаштування push-сповіщень | 1 день | Silent push для фонової синхронізації |
| Тестування на декількох пристроях | 2–3 дні | Навантажувальне тестування, виправлення багів |
| Деплой та моніторинг | 1 день | Доступ до консолі CloudKit, дашборд помилок |
Терміни: від 2 до 4 тижнів. Вартість розраховується індивідуально. Отримайте консультацію – ми оцінимо ваш проект за один день. Зв'яжіться з нами, щоб обговорити деталі.
Ми займаємося iOS-розробкою понад 10 років, реалізували 30+ проєктів із синхронізацією через CloudKit. Гарантуємо безшовну синхронізацію між пристроями протягом 24 годин після деплою. Замовте інтеграцію CloudKit вже сьогодні та позбудьтеся проблем із синхронізацією.
Apple CloudKit Documentation







