Синхронизация iCloud: CloudKit и NSUbiquitousKeyValueStore

Когда пользователь меняет устройство, данные приложения должны переноситься автоматически — иначе вы теряете клиента. Мы интегрируем синхронизацию iCloud через CloudKit и NSUbiquitousKeyValueStore, реализуя проект под ключ с учётом дельта-обновлений и обработки конфликтов. Наша команда обеспечивает надёжную синхронизацию и последующую поддержку, чтобы данные оставались актуальными на всех устройствах без сбоев.

Разработка и поддержка любых видов мобильных приложений:

Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

Это лишь некоторые из типы мобильных приложений, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента.

Услуги, которые мы предлагаем
Показано 1 из 1Все 1734 услуг
Синхронизация iCloud: CloudKit и NSUbiquitousKeyValueStore
Средний
~3-5 дней
Часто задаваемые вопросы

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

Как настроить дельта-синхронизацию: пошаговая инструкция

  1. Создайте подписку на изменения записей (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)
    }
  2. Реализуйте обработку 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
    }
  3. Используйте 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

Наши компетенции:

Последние работы

  • Разработка мобильного приложения для компании FEEDME
    Разработка мобильного приложения для компании FEEDME
    1009
  • Разработка мобильного приложения для компании XOOMER
    Разработка мобильного приложения для компании XOOMER
    902
  • Разработка мобильного приложения для компании RHL
    Разработка мобильного приложения для компании RHL
    1331
  • Разработка мобильного приложения для компании ZIPPY
    Разработка мобильного приложения для компании ZIPPY
    1179
  • Разработка мобильного приложения для компании Affhome
    Разработка мобильного приложения для компании Affhome
    1106
  • Разработка мобильного приложения для компании FLAVORS
    Разработка мобильного приложения для компании FLAVORS
    713