Реализация синхронизации данных через 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 недели. Стоимость рассчитывается индивидуально.







