Реализация синхронизации данных через iCloud

TRUETECH занимается разработкой, поддержкой и обслуживанием мобильных приложений iOS, Android, PWA. Имеем большой опыт и экспертизу для публикации мобильных приложений в популярные маркеты Google Play, App Store, Amazon, AppGallery и другие.
Разработка и поддержка любых видов мобильных приложений:
Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 1735 услуг
Реализация синхронизации данных через iCloud
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    756
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    624
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1052
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    947
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    862
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    445

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