Отметим: когда пользователь меняет 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







