Упрощаем работу с CloudKit, или синхронизация в духе Zen

    Введение


    Облачная синхронизация — закономерный тренд нескольких последних лет. Если вы разрабатываете под одну или несколько Apple платформ (iOS, macOS, tvOS, watchOS) и задачей является реализация функционала синхронизации между приложениями, то в вашем распоряжении есть очень удобный инструмент, или даже целый сервис — CloudKit.

    Нашей команде постоянно приходится прикручивать функционал синхронизации данных с CloudKit, в том числе в проектах, которые используют CoreData в качестве хранилища данных. Поэтому возникла, а затем была реализована идея — написать универсальный интерфейс для синхронизации.

    CloudKit — это не просто фреймворк. Это полноценный BaaS (Backend as a Service), т.е. комплексный сервис с полноценной инфраструктурой, включающей в себя облачное хранилище, пуш-уведомления, политики доступа и многое другое, а также предлагающий универсальный кросс-платформенный программный интерфейс (API).

    CloudKit прост в использовании и сравнительно доступен. Только за то, что вы являетесь участником Apple Developer Program, в вашем распоряжении совершенно бесплатно:

    • 10Gb хранилище под ресурсы
    • 100MB под базу данных
    • 2GB трафика в день
    • 40 запросов в секунду

    И эти цифры могут быть увеличены, если есть такая потребность. Стоит отметить, что CloudKit не использует iCloud-хранилище пользователя. Последний используется только для аутентификации.

    Эта статья — не реклама CloudKit и даже не очередной обзор основ работы с ним. Здесь не будет ничего о настройке проекта, конфигурировании App ID в вашем профиле разработчика, создании CK-контейнера или Record Type в дэшборде CloudKit. Кроме того, за рамками статьи остаётся не только backend составляющая, но и вся программная, относящаяся непосредственно к CloudKit API. Если вы хотели бы разобраться именно в основах работы с CloudKit, то для этого уже есть прекрасные вводные статьи, повторять которые нет никакого смысла.


    Эта статья — в некотором смысле следующий шаг.

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

    Универсальный интерфейс


    Создавая фреймворк, нашей конечной целью была реализация такого интерфейса, который был бы совместим с сущностями CoreData, позволяя при минимуме усилий синхронизировать — сохранять, удалять и получать данные — с учетом имеющихся связей БД, вне зависимости от сложности имеющейся архитектуры.

    Фреймворк написан на Swift 3 и именно Swift-разработчики в полной мере сумеют ощутить преимущества, которые даёт его использование. Для Objective-C возможен вполне полноценный bridge, но по известным причинам аналогичные вещи будут выглядеть в нём избыточными и более громоздкими в реализации. Примеры кода в данной статье будут написаны на Swift.

    Перейдём к обзору, параллельно рассматривая пример реализации.

    Пример реализации


    Рассмотрим в качестве введения некоторые типичные операции синхронизации: методы сохранения и удаления. Конечная реализация выглядит следующим образом:



    Что же здесь происходит?

    Положим, у нас есть объект event со свойством entity, где entity — это NSManagedObject. У этого NSManagedObject, как и у всякого объекта базы данных, есть поля, некоторые из которых являются свойствами, некоторые — ссылками, reference, на другие объекты NSManagedObject, образуя связи один-к-одному или один-ко-многим.

    Чтобы сохранить этот объект (или удалить соответствующий ему) синхронно или асинхронно в базу данных CloudKit, пробросив при этом все связи, используется прокси-объект — iCloud, который содержит в себе соответствующие методы. Достаточно вызывать entity.iCloud.save() (асинхронное) или entity.iCloud.saveAndWait() (синхронное сохранение), чтобы все поля entity были записаны в соответствующие поля объекта CloudKit, а уникальный UUID от вновь сохраненного CKRecord (т.е. строковое свойство recordName объекта CKRecordID) был автоматически записан обратно в специально отведенное для этого поле объекта entity, образовав тем самым связь между локальным и удаленным объектом.

    Если вы никогда не использовали CloudKit и всё это звучит непонятно, то проще сказать, что на любую сущность есть .iCloud.save() и этого достаточно, чтобы сохранить как сам объект, так и все его связи. Никакого больше множества идентичных методов для разных сущностей и грязи в клиентском коде. Удобно, не правда ли?

    Настройка объектов синхронизации


    Для того чтобы это работало, необходимо выполнить несколько условий.

    В основе работы лежит широко применяемая схема маппинга свойств, которая используется во многих библиотеках, в различных веб-парсерах (таких как RestKit) и т.д. Маппинг же реализован в классической манере — посредством KVC, который поддерживается только наследниками NSObject. Отсюда, первое условие:

    1) Каждый синхронизируемый объект должен быть наследником NSObject (к примеру, NSManagedObject – это отличный выбор).

    2) Каждый синхронизируемый объект должен реализовать протокол ZKEntity, который выглядит следующим образом:



    Если вы работаете с CoreData, то реализовывать нужно прямо в вашем (sub-)классе:



    Как видно из протокола, обязательными полями являются recordType и mappingDictionary. Рассмотрим оба.

    // REQUIRED (обязательные поля)

    1) recordType — соответствующий тип записи, Record Type, в CloudKit.

    Пример: класс Person содержит свойство recordType = “Person”. После вызова save() у его экземпляра, в дэшборде CloudKit именно в этой таблице (“Person”) будет заведена запись.

    Реализация:

    static var recordType = "Person"

    2) mappingDictionary — словарь маппинга свойств.
    Схема: [локальный ключ: удаленный ключ (поле в таблице CloudKit) ].

    Пример: класс Person содержит поля firstName и lastName. Чтобы сохранять их в таблицу Person в CloudKit под теми же именами, необходимо написать следующее:

    static var mappingDictionary = [ "firstName" : “firstName”, “lastName” : “lastName” ]


    // OPTIONAL (необязательные поля)

    Остальные поля протокола являются опциональными,

    3) syncIdKey — имя локального свойства, которое будет хранить ID удаленного объекта. ID — это паспорт объекта, необходимый для связи локальный<—>удаленный.

    Поле является условно опциональным. При инициализации контроллера фреймворка, о которой будет написано ниже, есть возможность указать имя свойства для всех сущностей. Однако, указанное индивидуально в классе сущности, оно имеет более высокий приоритет и при парсинге будет проверяться сначала именно оно. И лишь затем, если реализация пустая, будет использоваться универсальный ключ (см. далее).

    Реализация:

    static var syncId: String = "cloudID"

    changeDateKey — имя локального свойства, которое будет хранить дату изменения объекта. Ещё одно служебное свойство, необходимое для синхронизации.

    Аналогично предыдущему, оно условно опционально. Есть возможность опустить реализацию и указать имя свойства для всех синхронизируемых объектов во время инициализации ZenCloudKit (см. далее).

    Реализация:

    static var changeDateKey: String = "changeDate"

    references — словарь, содержащий ключи, реализующие связь *-к-одному.
    Схема: [“локальный ключ”: “удаленный ключ”]

    Требованием здесь является то, чтобы свойство “локальный ключ” своим типом имело класс, который удовлетворяет базовым требованиям (наследует NSObject и реализует протокол ZKEntity).

    При вызове save() у локального объекта ZenCloudKit попытается также сохранить все связанные с ним.

    Реализация:

    static var references : [String : String] =
                                            ["homeAddress" : "address"]

    referenceLists — словарь, содержащий массив объектов ZKRefList, каждый из которых несёт в себе информацию о конкретной связи *-ко-многим: тип объектов и название ключа, по которому необходимо запрашивать и сохранять этот список.

    Схема: ZKRefList(entityType: ZKEntity.Type,
    localSource: локальное свойство, которое возвращает массив объектов ZKEntity,
    remoteKey: ключ в CloudKit для хранения массива ссылок (CKReference))

    Реализация:

                static var referenceLists: [ZKRefList] = [ZKRefList(entityType: Course.self,
                                                        localSource: "courseReferences",
                                                        remoteKey: "courses")]


    courseReferences – это user-defined свойство, возвращающее массив объектов ZKEntity, которые вы хотели бы сохранить и ссылки на которые необходимо поместить в перечень ссылок корневого объекта.

    Код (продолжение):

     var courseReferences : [Course]? {
                 get {
                     return self.courses?.allObjects as? [Course]
                 }
         
                 set {
                     DispatchQueue.main.async {
                         self.mutableSetValue(forKey: "courses").removeAllObjects()
                         self.mutableSetValue(forKey: "courses").addObjects(from: newValue!)
                     }
         
                 }

    Реализация соответствующего сеттера также необходима чтобы приложение могло сохранить объекты, полученные из CloudKit. Таким образом, поле localSource объекта ZKRefList в сущности является ссылкой на обработчик (хэндлер), который управляет операциями ввода и вывода.

    isWeak — опциональный флаг, который, будучи установленным (true), указывает на то, что любой другой объект, ссылающийся на экземпляр данного типа, образует слабую ссылку (аналогия с модификатором weak) в CloudKit. Это означает, что запись о нём будет удалена каскадно, как только будет удален объект, который содержит ссылку на него.

    Пример: есть объект A, ссылающийся на объект B.

    Если установить B.isWeak = true, объект А будет сохранен в CloudKit со “слабой ссылкой” на B. Объект B будет удален автоматически, как только вы удалите объект A.

    Этот флаг является реализацией нативного API CloudKit и апеллирует к конструктору CKReference с флагом .deleteSelf:

    CKReference.init(record: <CKRecord>, action: .deleteSelf)

    Поэтому механика удаления — целиком прерогатива CloudKit, фреймворк же просто предлагает более удобный интерфейс. В дальнейшем этот функционал может быть расширен, чтобы каскадное удаление можно было настраивать для разных сущностей.

    Реализация:

    
    static var isWeak = true

    referencePlaceholder — свойство, которое, будучи объявленным, позволяет избежать значения nil при получении объекта из CloudKit, подменяя его значением по умолчанию.

    Если предполагается, что объект сущности CoreData должен всегда содержать какое-либо значение, отличное от nil, в качестве ссылки на другой объект, то всякий раз, когда данный объект будет отсутствовать в CloudKit при синхронизации, локальному свойству может быть автоматически задано значение по умолчанию.

    Пример: есть класс A со свойством b и, зеркально ему, такой же Record Type в CloudKit.

    В CloudKit имеется объект A, который отсутствует локально, имеющий пустую ссылку на B (значение отсутствует). При обычном сценарии в результате синхронизации вы бы получили объект A, у которого свойство b было бы nil. Но с установленным значением по умолчанию в локальном классе (referencePlaceholder = …) ZenCloudKit автоматически присвоит свойству b указанное вами значение:

    A.b = referencePlaceholder,

    где последний является экземпляром B.

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

    Реализация:

    static var referencePlaceholder: ZKEntity = B.defaultInstance()

    Обратите внимание, что referencePlaceholder указывается именно в таргет-классе. Если нужно, чтобы свойство b объекта A не оказывалось nil (A.b != nil), то именно в классе B необходимо реализовать referencePlaceholder, а не в корневом классе A, который мы получили в результате синхронизации.

    // SUMMARY

    На момент написания статьи это весь функционал, поддерживаемый ZKEntity. Подытожим изложенное ещё раз в виде конкретного примера.

    Положим, есть класс Event:


    Реализация ZKEntity может выглядеть, например, так:



    Здесь:

    • словарь для маппинга свойств.
    • словарь для маппинга ссылок (опционально)
    • CloudKit Record Type

    Опущены syncIdKey и changeDateKey. В примере им соответствуют свойства syncID и changeDate. Поскольку аналогичные свойства (changeDate, syncID) присутствуют в интерфейсе других классов, они были записаны на фазе инициализации ZenCloudKit (о чём пойдёт речь далее) как универсальные, поэтому частная имплементация была опущена.

    Настройка контроллера и делегата


    После того, как сущности были настроены, необходимо проинициализировать контроллер и назначить его делегат. Сделать это можно различными способами, но лучше всего — отвести для этого отдельный класс и написать вызываемый инициализатор.

    Для начала можно завести глобальную переменную, которая будет хранить ссылку на статический экземпляр контроллера.



    Класс-делегат должен будет реализовать следующий протокол:



    Прежде чем рассматривать каждый метод в отдельности, попробуем взглянуть на вариант готовой реализации (за исключением метода zenSyncDIdFinish).



    Класс CloudKitPresenter в приведенном примере является делегатом ZenCloudKit. Здесь происходит инициализация и вызов callback-функций, необходимых для реализации полного цикла синхронизации. Полный цикл синхронизации — это последовательность закадровых операций, при которых осуществляется сравнение локальных и удаленных объектов по времени изменения и их актуализация на обоих концах. Для этого по каждому типу, т.е. по каждой зарегистрированной сущности ZKEntity, фреймворку необходимо предоставить три функции, реализующие соответственно создание, запрос объекта по ID (fetch) и запрос всех доступных объектов. В каждой из трёх функций в качестве параметра выступает класс ZKEntity (ofType T: ZKEntity.Type). В результате выполнения ZenCloudKit ожидает получить объекты именно данного типа.

    zenAllEntities(ofType T: ZKEntity.Type)
    — ожидает получить массив всех сущностей типа T

    zenCreateEntity(ofType T: ZKEntity.Type)
    — ожидает получить новый экземпляр T.

    zenFetchEntity(ofType T: ZKEntity.Type, syncId: String)
    — ожидает получить существующий экземпляр T по данному syncId (или nil если таковой отсутствует).

    Например, если вы работаете с сущностями Person и Home, то параметр T в данных функциях будет равен одному из этих двух типов. Ваша задача — предоставить результат по каждому из них (новый объект, существующий и все). Сделать это можно либо осуществив проверку типа и написав код для каждого, либо при помощи интерфейсного полиморфизма.

    В приведенном примере для осуществления перечисленных операций используются стандартные методы MagicalRecord для поиска существующего, создания нового и запроса всех объектов, которые работают как extension-методы (или методы категорий, выражаясь в духе Objective-C) для NSManagedObject. Это значительно упрощает реализацию. Код становится универсальным, поскольку пропадает нужда делать type-check для каждого случая T.

    Функции являются конкретной реализацией generic-абстракции, хотя, строго говоря, обобщения в сигнатуре функций не используются в целях обеспечения совместимости с Objective-C.

    В последней функции используется инструкция T.predicateForId(…). Это метод расширения, предоставленный ZenCloudKit, который возвращает корректный предикат поиска для данного типа T по данному syncId (чтобы избежать хард-кода и связанных с ним возможных ошибок в названии свойства, локально хранящего ID).

    zenEntityDidSaveToCloud (entity: ZKEntity, record: CKRecord?, error: Error?)
    — вызывается каждый раз при завершении сохранения в CloudKit. На этой фазе объект entity уже получил ID удаленного объекта, поэтому здесь можно, например, сохранить главный контекст базы данных.

    Делегат реализует закрытый Singleton (sharedInstance не виден клиенту). Для того, чтобы проинициализировать и контроллер, и его делегат, достаточно где-либо извне в нужный момент вызвать метод:



    В методе инициализации происходит настройка фреймворка:



    Задаются стандартные для CloudKit параметры:

    • имя контейнера (container)
    • тип базы данных (ofType: .public/.private)

    Далее следуют уже рассмотренные выше ключи syncIdKey и changeDateKey — имена свойств, хранящих ID записей и дату изменения. Необходимо отметить, что эти значения могут быть оставлены пустыми (nil). В таком случае при вызове соответствующих методов у экземпляров ZKEntity (например, save()) ZenCloudKit будет искать их имплементацию среди объявлений каждого класса. И наоборот, достаточно указать эти ключи только здесь, чтобы опустить специфичную реализацию. Если пустой окажется и общая, и частная имплементация, то вызов cloudKit.setup() выдаст в лог ошибку, и синхронизация работать не будет.

    В параметр entities мы передаем массив всех типов, с которыми собираемся работать.

    ignoreKeys — массив строковых ключей, обнаружив которые, ZenCloudKit должен проигнорировать объект (например, не сохранять или не удалять его).

    deviceId — ID устройства. Очень важный параметр, если в синхронизации будет задействовано несколько устройств. Об уникальности этого параметра должен позаботиться разработчик. Стандартно, берётся Hardware UUID, но возможны и другие варианты.

    // RECAP

    Реализация настроек, описанных до сих пор, является необходимым и достаточным условием для того, чтобы работал базовый функционал, предоставленный прокси-объектом iCloud, который, в свою очередь, реализует протокол ZKEntityFunctions:


    За исключением функции update(), назначение которой — обновить локальный объект из удаленного, представленного в коде как CKRecord. Эту функцию следует использовать в методе делегата zenSyncDIdFinish, который вызывается по окончании полного цикла синхронизации, который, в свою очередь, запускается следующим образом:


    Первый вариант — синхронизация в стандартном режиме. Каждый последующий цикл синхронизации фиксируется ZenCloudKit; в случае успеха, сохраняется дата последней синхронизации (всё это берёт на себя фреймворк). Сохранение даты очень важно: оно позволяет отбирать только те объекты, дата изменения которых — позже даты последнего успешного цикла. В противном случае, если, скажем, у вас в БД 100 объектов, то каждый цикл включал бы бессмысленную проверку давно уже синхронизированных, не изменяющихся объектов. Это совершенно не нужная и, к тому же, ресурсозатратная операция.

    Второй вариант — принудительная синхронизация (forced: true). Могут быть случаи, когда целостность данных оказывается нарушенной. Тогда вы можете в принудительном порядке проверить каждый синхронизируемый объект, игнорируя дату последнего успешного цикла, и актуализировать данные локально и удаленно. Локальные объекты будут обновлены тем, что лежит в CloudKit (если по каким-то причинам этого не произошло ранее). А в CloudKit могут быть сохранены локальные объекты, которые также почему-то не были сохранены. В зависимости от специфики вашего приложения, вы сами можете определить, в каком месте вызывать принудительную синхронизацию (например, при старте, во время длительного простоя или же отвести эту функцию в настройки). В общем случае в этом вызове нет нужды и, скорее всего, вам не придётся к нему прибегать.

    Вызов метода syncEntities() на уровне контроллера делает то же самое, только применительно ко всем зарегистрированным сущностям. Параметр specific принимает конкретные типы, которые вы бы хотели синхронизовать (nil — если нужно применить ко всем).

    Осталось разобрать метод zenSyncDIdFinish, сигнатура которого выглядит так:


    Параметры:

    T — тип сущности, объекты которой необходимо создать или обновить.

    newRecords, updatedRecords — массивы CKRecord, объектов, которые необходимо создать или обновить локально. Ориентиром при поиске локального соответствия выступает уникальный ID, который стандартно хранится в свойстве CKRecord.recordID.recordName. Сущность, среди объектов которой нужно искать соответствия и экземпляр которой создать, является T.

    deletedRecords — массив объектов ZKDeleteInfo, каждый из которых хранит информацию об удаляемом объекте: локальный ZKEntity-тип и ID объекта. Эти объекты могут быть различных типов, поэтому ориентироваться на тип T в данном случае не нужно. Тип удаляемого объекта следует смотреть в свойстве entityType, а ID объекта — в свойстве syncId объекта ZKDeleteInfo. Класс выглядит следующим образом:


    ZenCloudKit формирует этот список перед тем, как завершить удаление, отправляя его в обработчик zenSyncDidFinish в массиве deletedRecords, чтобы вы смогли произвести необходимую локальную очистку. Как только локально всё будет успешно удалено, необходимо вызвать callback-метод finishSync(). Если этого не сделать, то в базе данных CloudKit не будет произведено никаких изменений. Такая схема принята в целях безопасности: лишь удостоверившись в том, что локальная база данных актуализирована, вы вызываете финализатор — finishSync().

    Всегда вызывайте finishSync() в конце синхронизации.

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

    Подытожим сказанное, рассмотрев фрагмент реализации функции zenSyncDIdFinish:


    Сразу после данного фрагмента должны следовать:

    — вызов finishSync()
    — функции обновления UI, которые бы отразили изменившееся состояние БД (если требуется).

    При помощи следующей инструкции:



    мы заполняем поля локального объекта полями CKRecord, который нам доступен как аргумент в одном из массивов. Флаг fetchReferences позволяет загрузить все связи. Под загрузкой связей подразумевается реальная загрузка соответствующих объектов (приведенных в массивах references и referenceLists, описанных в протоколе ZKEntity) из CloudKit и их привязка к данному объекту entity. Если при загрузке связи обнаружится, что соответствующий локальный объект не существует (zenFetchEntity == nil), он будет автоматически создан в локальной базе данных путём вызова метода делегата zenCreateEntity.

    Если образование этих связей предполагает изменение UI, об этом необходимо позаботиться дополнительно (updateEntity — в части заполнения связей — работает асинхронно и дожидаться его выполнения не стоит). В обработчике ZKRefList это можно сделать в сеттере, о чём уже говорилось:


    Здесь происходит следующее:

    При получении связей *-ко-многим (в результате вызова updateEntity с флагом fetchReferences = true) в сеттер teacherReferences попадает массив объектов Teacher. В главном потоке мы обновляем этот список у корневого объекта NSManagedObject, а затем вызываем методы обновления UI.

    Маппинг связей *-к-одному (массив references, содержащий название свойств-ссылок на другие сущности ZKEntity) не предполагает обработчиков (get/set), поэтому, если требуется отслеживать образование этих связей, необходимо прибегнуть либо к аналогичному методу — в качестве ключей в массиве references указывать обработчики и переопределять их геттер и сеттер, — либо использовать ReactiveCocoa или иные средства для наблюдения за свойствами.

    Работа со ссылками кажется богатой нюансами, и это действительно так, однако эти нюансы — закономерное следствие обвязки и автоматизации работы двух систем — CoreData и CloudKit.

    Если вам нужно иметь более прямой контроль над образованием связей, обновлением UI или другими sync-related процессами, по усмотрению вы можете совместить средства ZenCloudKit и нативный CloudKit API. В методе zenSyncDidFinish передаются массивы объектов CKRecord, которые, помимо свойств, содержат объекты CKReference. Это значит, что вы можете кастомизировать парсинг, а также вручную загрузить те объекты, которые вам нужны.

    На этом настройка ZenCloudKit окончена.

    Нюансы использования


    Стандартный способ обращения к функционалу фреймворка — через экземпляр (singleton) ZenCloudKit контроллера:


    В качестве аргументов — всё те же экземпляры и классы ZKEntity.
    Сокращенный вариант (через прокси-класс .iCloud) в данный момент доступен только в Swift.

    Push-уведомления


    Обработка push-уведомлений также может быть передана в ZenCloudKit:


    Результатом его работы будет вызов метода делегата zenSyncDIdFinish, с одним из трёх заполненных массивов (newRecords, updatedRecords, deletedRecords), выполнение которого автоматически приведет к обновлению базы данных и UI (если вы позаботились об этом в теле данной функции). Напомню, что обычный сценарий обработки push-уведомлений предполагает ряд довольно монотонных действий: проверка типа уведомления (CKNotification), причины нотификации (queryNotificationReason), парсинг — определение сущности, к которой относится уведомление и лишь затем вызов соответствующего обработчика. ZenCloudKit берёт всё это на себя.

    Блокировка синхронизации


    Рано или поздно код вашего приложения будет наполнен инструкциями .save() или .delete() в разных местах. Если вы предполагаете возможность отключения синхронизации изнутри приложения (а не в свойствах системы), то вместо того чтобы в каждом месте клиентского кода делать проверку какого-нибудь флага, вы можете отключить синхронизацию на уровне фреймворка:



    Возобновление синхронизации, как можно догадаться, достигается передачей false. И ваш код остается чистым.

    Логгирование


    Все основные этапы работы фреймворка логгируются. Включение/отключение флага debugMode позволяет управлять выводом в консоль служебной информации (по умолчанию true):



    Настройка контейнера:

    Для успешной работы приложению и ZenCloudKit необходим доступ на чтение и запись всех используемых Record Type, включая query-права на ключ modifiedDate (CKRecord). Не забудьте включить всё это в дэшборде. Кроме того, фреймворком в базе данных будут созданы таблицы под названием Device и DeleteQueue. Первая будет содержать список зарегистрированных устройств, которые обращаются к вашей базе данных. Вторая — очередь на удаление — представляет собой таблицу с мета-информацией об удаленных объектах, которые необходимо удалить на каждом устройстве (для каждого устройства — соответствующая запись). После того, как это устройство осуществит локальное удаление соответствующего объекта, запись из DeleteQueue также будет стёрта. Эти две таблицы являются служебными, к ним должен быть полный доступ на чтение и запись для каждого устройства.

    Безопасность


    Последним достойным внимания моментом работы ZenCloudKit является безопасность.
    Процедура сохранения объектов в CloudKit стандартно сводится к двум этапам: (1) проверка на наличие искомого объекта, и только затем — (2) сохранение. Рассмотрим ситуацию, когда в кратчайший промежуток времени вы атомарно сохраняете 15 новых объектов (или один и тот же несколько раз подряд), или же это происходит в результате сбоя. В стандартном сценарии работы CloudKit это может произойти так: сначала несколько раз сработает хэндлер поиска (fetch), возвратив nil, а затем столько же раз будет вызвана команда сохранения (ведь объект не найден). В результате, не желая того, вы получите несколько экземпляров одного и того же объекта в CloudKit. Без дополнительных мер (см. GCD), это неизбежно, потому что CloudKit API основан на асинхронных блоках, последовательность которых сложно предугадать, даже выставив флаги приоритета и QoS у CKQueryOperation.

    Описанного выше сценария гарантированно не случится с ZenCloudKit, который на этапе инициализации создает очередь для каждого зарегистрированного типа ZKEntity, обеспечивая строгую последовательность в выполнении операций сохранения. Если среди 15 объектов — по 3 объекта разных типов (итого 5 типов), то при их одновременном сохранении, “в одно время” будет запущен процесс сохранения для 5 объектов, без какой-либо угрозы. Также схема сводит на нет возможность отказа (DoS).

    Заключение


    Фреймворк создавался одним человеком в течение примерно двух месяцев. Значительная часть времени была потрачена не столько на программирование, сколько на дизайн и рефакторинг. Цель стояла простая — упростить и унифицировать выполнение типовых операций синхронизации с CloudKit, обеспечив приемлемый уровень совместимости с CoreData. Кардинальных неисправностей и серьезных багов в ходе применения на сегодняшний день обнаружено не было.

    Некоторые функции в данной статье не описаны (например, управление потерянным соединением и автоматический запуск полного цикла синхронизации, по мере его восстановления). Известны также некоторые нюансы: например, на данный момент отсутствует поддержка CKAssets (однако её реализовать не сложно).

    В данный момент фреймворк вместе с демо-проектом готовится на выкладку. Если вы хотели бы получить исходной код ZenCloudKit или у вас есть какие-либо вопросы или комментарии, будем рады узнать о них в комментариях к данной статье или через ЛС.
    Everyday Tools 284,16
    Утилиты на все случаи жизни
    Поделиться публикацией
    Комментарии 2
    • 0
      Очень неплохая статья по пользованию ZenCloudKit framework, более менее понятно объяснено как и зачем что используется. Вот только не хватает маленькой детали. Самого фреймворка, а еще лучше его исходников. По большому счету все как раз шло к его открытию, но что-то, видимо, пошло не так. В общем очень жду демо проект, чтобы в руках таки покрутить и по разбирать детально ваше творение.
      • 0
        Фреймворк вместе с демо-проектом будет скоро выложен. Чуточку терпения. :) Спасибо за интерес.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое