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







