Введение


Облачная синхронизация — закономерный тренд нескольких последних лет. Если вы разрабатываете под одну или несколько 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 или у вас есть какие-либо вопросы или комментарии, будем рады узнать о них в комментариях к данной статье или через ЛС.
Поделиться с друзьями
-->

Комментарии (2)


  1. MrGrig192
    14.04.2017 09:06

    Очень неплохая статья по пользованию ZenCloudKit framework, более менее понятно объяснено как и зачем что используется. Вот только не хватает маленькой детали. Самого фреймворка, а еще лучше его исходников. По большому счету все как раз шло к его открытию, но что-то, видимо, пошло не так. В общем очень жду демо проект, чтобы в руках таки покрутить и по разбирать детально ваше творение.


    1. Hexfire
      18.04.2017 05:44

      Фреймворк вместе с демо-проектом будет скоро выложен. Чуточку терпения. :) Спасибо за интерес.