- Build Apps Using CloudKit
- Overview
- CloudKit Console
- Упрощаем работу с CloudKit, или синхронизация в духе Zen
- Введение
- Универсальный интерфейс
- Пример реализации
- Настройка объектов синхронизации
- Настройка контроллера и делегата
- Нюансы использования
- Push-уведомления
- Блокировка синхронизации
- Логгирование
- Безопасность
- Заключение
Build Apps Using CloudKit
Store your app’s data in iCloud and keep everything up-to-date across devices and on the web. Featuring efficient syncing, as well as simple monitoring and management, it’s never been easier to build and grow your apps with CloudKit. Store private data securely in your users’ iCloud accounts for limitless scale as your user base grows, and get up to 1PB of storage for your app’s public data.
Overview
CloudKit is designed for manageability, flexibility, and power. By organizing apps in containers, CloudKit ensures each app is siloed so its data won’t get entangled with other apps. Specialized databases and zones also let you easily separate app information by access type or function. And together with efficient syncing and sharing capabilities, CloudKit provides a comprehensive feature set that lets you easily develop powerful cloud apps.
- Automatic syncing. Seamlessly syncs across iOS, iPadOS, macOS, tvOS, watchOS, and the web.
- Encrypted data. Configure fields in private CloudKit databases to be encrypted, ensuring data protection in storage and in transport to your application.
- Protected privacy. Develop, analyze, and debug your apps without exposing your users’ personally identifiable data.
- Automation. Easily automate your development processes.
CloudKit Console
The CloudKit Console is an intuitive web-based control panel that you can use throughout the development lifecycle of your app. View your app’s server activity, manage your containers, maintain your database schema, and view and edit test data during every phase — from development to production.
- Database. Access data stored directly in CloudKit with the CloudKit Database app.
- Telemetry. View a collection of charts and measure the overall aggregate performance and reliability of your apps across all of your users’ devices.
- Server logs. Analyze and evaluate app performance with comprehensive and up-to-date log data while maintaining user privacy.
Источник
Упрощаем работу с 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”) будет заведена запись.
2) mappingDictionary — словарь маппинга свойств.
Схема: [локальный ключ: удаленный ключ (поле в таблице CloudKit) ].
Пример: класс Person содержит поля firstName и lastName. Чтобы сохранять их в таблицу Person в CloudKit под теми же именами, необходимо написать следующее:
// OPTIONAL (необязательные поля)
Остальные поля протокола являются опциональными,
3) syncIdKey — имя локального свойства, которое будет хранить ID удаленного объекта. ID — это паспорт объекта, необходимый для связи локальный удаленный.
Поле является условно опциональным. При инициализации контроллера фреймворка, о которой будет написано ниже, есть возможность указать имя свойства для всех сущностей. Однако, указанное индивидуально в классе сущности, оно имеет более высокий приоритет и при парсинге будет проверяться сначала именно оно. И лишь затем, если реализация пустая, будет использоваться универсальный ключ (см. далее).
changeDateKey — имя локального свойства, которое будет хранить дату изменения объекта. Ещё одно служебное свойство, необходимое для синхронизации.
Аналогично предыдущему, оно условно опционально. Есть возможность опустить реализацию и указать имя свойства для всех синхронизируемых объектов во время инициализации ZenCloudKit (см. далее).
references — словарь, содержащий ключи, реализующие связь *-к-одному.
Схема: [“локальный ключ”: “удаленный ключ”]
Требованием здесь является то, чтобы свойство “локальный ключ” своим типом имело класс, который удовлетворяет базовым требованиям (наследует NSObject и реализует протокол ZKEntity).
При вызове save() у локального объекта ZenCloudKit попытается также сохранить все связанные с ним.
referenceLists — словарь, содержащий массив объектов ZKRefList, каждый из которых несёт в себе информацию о конкретной связи *-ко-многим: тип объектов и название ключа, по которому необходимо запрашивать и сохранять этот список.
Схема: ZKRefList(entityType: ZKEntity.Type,
localSource: локальное свойство, которое возвращает массив объектов ZKEntity,
remoteKey: ключ в CloudKit для хранения массива ссылок (CKReference))
courseReferences – это user-defined свойство, возвращающее массив объектов ZKEntity, которые вы хотели бы сохранить и ссылки на которые необходимо поместить в перечень ссылок корневого объекта.
Реализация соответствующего сеттера также необходима чтобы приложение могло сохранить объекты, полученные из CloudKit. Таким образом, поле localSource объекта ZKRefList в сущности является ссылкой на обработчик (хэндлер), который управляет операциями ввода и вывода.
isWeak — опциональный флаг, который, будучи установленным (true), указывает на то, что любой другой объект, ссылающийся на экземпляр данного типа, образует слабую ссылку (аналогия с модификатором weak) в CloudKit. Это означает, что запись о нём будет удалена каскадно, как только будет удален объект, который содержит ссылку на него.
Пример: есть объект A, ссылающийся на объект B.
Если установить B.isWeak = true, объект А будет сохранен в CloudKit со “слабой ссылкой” на B. Объект B будет удален автоматически, как только вы удалите объект A.
Этот флаг является реализацией нативного API CloudKit и апеллирует к конструктору CKReference с флагом .deleteSelf:
Поэтому механика удаления — целиком прерогатива CloudKit, фреймворк же просто предлагает более удобный интерфейс. В дальнейшем этот функционал может быть расширен, чтобы каскадное удаление можно было настраивать для разных сущностей.
referencePlaceholder — свойство, которое, будучи объявленным, позволяет избежать значения nil при получении объекта из CloudKit, подменяя его значением по умолчанию.
Если предполагается, что объект сущности CoreData должен всегда содержать какое-либо значение, отличное от nil, в качестве ссылки на другой объект, то всякий раз, когда данный объект будет отсутствовать в CloudKit при синхронизации, локальному свойству может быть автоматически задано значение по умолчанию.
Пример: есть класс A со свойством b и, зеркально ему, такой же Record Type в CloudKit.
В CloudKit имеется объект A, который отсутствует локально, имеющий пустую ссылку на B (значение отсутствует). При обычном сценарии в результате синхронизации вы бы получили объект A, у которого свойство b было бы nil. Но с установленным значением по умолчанию в локальном классе (referencePlaceholder = …) ZenCloudKit автоматически присвоит свойству b указанное вами значение:
где последний является экземпляром B.
Так, в результате полного цикла синхронизации в вашем приложении всегда будут создаваться объекты с заполненными ссылками, даже в том случае, если на всех остальных устройствах они сохранялись пустыми.
Обратите внимание, что referencePlaceholder указывается именно в таргет-классе. Если нужно, чтобы свойство b объекта A не оказывалось nil (A.b != nil), то именно в классе B необходимо реализовать referencePlaceholder, а не в корневом классе A, который мы получили в результате синхронизации.
На момент написания статьи это весь функционал, поддерживаемый 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, но возможны и другие варианты.
Реализация настроек, описанных до сих пор, является необходимым и достаточным условием для того, чтобы работал базовый функционал, предоставленный прокси-объектом 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 или у вас есть какие-либо вопросы или комментарии, будем рады узнать о них в комментариях к данной статье или через ЛС.
Источник