Содержание
- Часть 1. Введение. О компании Shopping UK и о том, почему ее собственное решение для обмена данными необходимо заменить. (1 мая 2023 г.)
- Часть 2. Что такое CloudKit?: обзор концепций CloudKit и того, как была разработана схема Shopping UK. (8 мая 2023 г.)
- Часть 3: Добавление, обновление и удаление записей: как изменить данные в iCloud и различные варианты их получения. (15 мая 2023 г.)
- Часть 4: Принципы совместного использования и начало совместного использования. Знакомит с концепциями совместного использования и с тем, как начать совместное использование списка. (22 мая 2023 г.)
- Часть 5. Принятие приглашения к совместному использованию. Как другое устройство может принять приглашение для совместной работы над общим списком. (29 мая 2023 г.)
- Часть 6: Синхронизация данных: как данные синхронизируются между устройствами и некоторые проблемы, которые необходимо учитывать. (5 июня 2023 г.)
- Часть 7. Управление общим ресурсом и фоновое обслуживание: как изменить или остановить общий ресурс, что происходит при удалении списка, фоновое обслуживание. (12 июня 2023 г.)
- Часть 8. Когда что-то идет не так: обработка ошибок, объединение данных и диагностика проблем. (19 июня 2023 г.)
Это первая статья из серии из восьми статей о реализации обмена данными в Shopping UK с помощью CloudKit.
Часть 1: Введение
Идея проста: мы с женой делимся списком покупок. Когда я опорожняю последнюю банку меда, я добавляю «мед» в наш общий список. Когда моя жена допивает кофе, она делает то же самое. Делаем это на протяжении всей недели. Когда я делаю покупки, я просматриваю список, чтобы напомнить себе, что купить, и отмечаю товары, когда кладу их в тележку. Список на телефоне моей жены будет автоматически обновляться, чтобы отразить то, что я купил, чтобы она не покупала дубликаты, когда будет в следующий раз в магазине.
Я создавал и поддерживал Shopping UK в течение 10 лет, и это было скромным успехом. У него рейтинг 4,8 в App Store, более 45 000 человек регулярно используют приложение, и оно приносит ежемесячный доход от 1000 до 1500 фунтов стерлингов в месяц — от рекламы и премиальной подписки (в основном подписки).
Есть много подобных приложений, но для покупателей из Великобритании, пожалуй, проще всего использовать Shopping UK. Он знает почти каждый продукт, который вы, вероятно, купите в британском супермаркете, он автоматически упорядочит ваши товары по проходу в супермаркете, а с 2018 года он предлагает поддержку для обмена списком с семьей и друзьями.
Совместное использование списка покупок между двумя устройствами — очень простая для понимания концепция — во многих учебниках она используется в качестве примера при создании демонстрационного приложения. Но вскоре все становится немного сложнее. Дойти до 80% легко. Последние 20% занимают все время. В этой серии статей я хочу сосредоточиться на этих трудных 20%.
В 2018 году, когда я впервые представил функцию обмена списками покупок, я создал собственный облачный механизм синхронизации данных. В то время я по глупости думал, что мои потребности уникальны, и мне потребуется полный контроль над серверами для поддержки будущих функций. Кроме того, меня привлекала интересная техническая задача!
Я пожалел об этом решении.
Хотя мое самодельное решение для обмена списками работало, у него были серьезные недостатки:
- Расходы. Стоимость запуска сервера и сопутствующей базы данных SQL в Azure составляла около 80 фунтов стерлингов в месяц. В настоящее время это не сумасшедшая сумма, но я беспокоился, что если приложение станет более успешным, стоимость может резко возрасти. Может потребоваться большая база данных и дополнительные серверы приложений, и, поскольку я предлагаю общий доступ всем пользователям бесплатно, увеличение использования приложения не приведет напрямую к увеличению дохода. Разбивка стоимости хостинга: Служба приложений Azure: 55 фунтов стерлингов в месяц, SQL Server с 20 DTU: 22 фунта стерлингов в месяц, плюс пропускная способность, хранилище файлов и таблиц и аналитика. При увеличении нагрузки базу данных можно увеличить до 50 DTU по цене 55 фунтов стерлингов в месяц (DTU — это единица транзакции базы данных, единица измерения, представляющая смешанную меру ЦП, памяти, операций чтения и записи).
Разбивка стоимости хостинга: Служба приложений Azure: 55 фунтов стерлингов в месяц, SQL Server с 20 DTU: 22 фунта стерлингов в месяц, плюс пропускная способность, хранилище файлов и таблиц и аналитика. При увеличении нагрузки базу данных можно увеличить до 50 DTU по цене 55 фунтов стерлингов в месяц (DTU — это единица транзакции базы данных, единица измерения, представляющая смешанную меру ЦП, памяти, операций чтения и записи).
- Поддержка и обслуживание. Серверы управляются Azure, но мне пришлось самому поддерживать службы приложений и серверный код. Каждый год SSL-сертификат нужно было обновлять. Что делать, если служба перестала работать ночью или когда я был в отпуске? Вес этого навис надо мной. Поскольку платформы и фреймворки обновлялись или устарели, мне нужно было изменить и повторно протестировать свой код. График развертывания сервера отличался от графика развертывания приложения для iOS, и для него требовался совсем другой процесс и набор инструментов.
- Производительность. Совместное использование основывалось на механизме опроса. Каждые 10 секунд, пока приложение было активным, на сервер отправлялось сообщение, содержащее добавленные или измененные элементы. Сервер ответит изменениями, сделанными другими участниками списка. Это означало, что пользователям приходилось ждать до 20 секунд после того, как кто-то внес изменения, прежде чем их список обновлялся. В часы пик элементы не всегда синхронизировались с первой попытки, поскольку сервер был сильно загружен, и время ожидания некоторых запросов истекало. Вдобавок ко всему этому частые опросы приводили к разрядке батареи.
- Отсутствие гибкости. Серверный код был оптимизирован для первоначального дизайна. Это было быстро, но за счет гибкости. Добавление новых функций было затруднено, потому что код сервера должен был измениться вместе с кодом клиента (и поскольку существуют десятки тысяч пользователей, старая и новая версии должны поддерживаться одновременно). Этот «налог на изменения» ограничивал функции, которые я хотел добавить, такие как добавление изображений к элементам списка, совместное использование пользовательских категорий, отправка уведомлений другим пользователям, когда элемент добавляется или вычеркивается из списка покупок.
Требовалась серьезная работа, и она потребовала бы больших затрат сил. Это означало, что я должен был правильно рассчитать время. В 2020 году я планировал начать работу над механизмом обмена на основе CloudKit, когда разразился COVID. В своей основной работе я работаю архитектором и консультантом системы управления лабораторной информацией (LIMS). Во время кризиса COVID все стало очень занятым. Меня привлекли для разработки и создания решений, помогающих отслеживать распространение COVID в Великобритании, а это означало, что побочный проект пришлось отложить.
Как только кризис Covid утих, у меня было несколько месяцев более спокойного времени, чтобы изучить CloudKit и создать новое решение для обмена списками.
В этой серии из восьми частей я опишу дизайн готового решения, которое будет включать в себя:
- Основные концепции CloudKit.
- Как данные перемещаются между устройством и iCloud.
- Как инициировать совместное использование и как приглашение к совместному использованию принимается приложением.
- Стратегии синхронизации данных между устройствами: когда и как синхронизировать.
- Как управлять участниками обмена, что происходит, когда вы прекращаете делиться, выходите из списка или выходите из iCloud.
- Советы по диагностике проблем, обработке пограничных случаев, параллелизму и ведению журнала.
- На следующей неделе мы начнем со знакомства с основными концепциями CloudKit и дизайном схемы данных.
Это вторая статья из серии из восьми статей о реализации обмена данными в Shopping UK с помощью CloudKit.
Часть 2. Что такое CloudKit?
Shopping UK — это умный список покупок для покупателей из Великобритании. Он знает почти каждый товар в супермаркете и расставит их по проходам. Списки могут быть разделены с семьей или друзьями.
На прошлой неделе мы представили Shopping UK и обсудили ограничения его старого доморощенного решения для обмена данными. Сегодня мы рассмотрим основные концепции CloudKit и то, как разработана схема Shopping UK.
Концепции
Мы начнем с краткого обзора основных концепций CloudKit.
Если вы уже знакомы с концепциями, перейдите к разделу «Создание схемы CloudKit» ниже, в котором объясняется, как CloudKit используется в Shopping UK.
CloudKit — одна из ключевых технологий Apple для обмена данными в облаке. Apple предлагает три варианта обмена данными:
- iCloud Document Storage для синхронизации файлов или документов.
- iCloud Key-Value Storage для небольших пар «ключ-значение».
- CloudKit для обмена сложными объектами.
CloudKit — это способ перемещения структурированных данных между вашим приложением и iCloud. Он также имеет веб-панель для просмотра и управления данными в вашей собственной учетной записи iCloud (это действительно полезно для тестирования).
CloudKit можно использовать бесплатно. Он изначально поддерживается в iOS, а также имеет JavaScript API, поэтому его также можно использовать с веб-сайтов и за пределами экосистемы Apple.
Давайте рассмотрим три ключевых понятия: записи, типы записей и зоны записи.
Записи
Данные хранятся в CKRecord. Думайте об этом как о записях в реляционной системе баз данных.
Каждая запись CKRecord имеет тип записи — шаблон для определения того, какие значения может содержать запись CKRecord. Тип записи имеет имя и список ключей. Каждому ключу назначается тип данных (например, String, Date, Bool).
Думайте о типе записи как о таблице в системе реляционной базы данных, а о ключах — как о полях.
Типы записей
Для хранения книг автора мы можем создать два типа записей: Книга и Автор. Книга имеет название, дату публикации, количество страниц, номер ISBN и ссылку на автора:
Вместе тип записи описывает «форму» данных и то, как они связаны друг с другом — его схему. Сами данные представлены CKRecord. В этом примере используется пара моих книг одного из моих любимых авторов детства, Роальда Даля:
Зоны записи
CKRecordZone — это группа CKRecords.
Все CKRecords живут в одной и только одной CKRecordZone.
CKRecordZone находится в CKDatabase. Существует три базы данных: Public, Private и Shared (подробнее об этом позже).
Каждая общедоступная и частная базы данных CKDatabase имеет CKRecordZone по умолчанию.
Новые настраиваемые CKRecordZones можно создавать в частных и общих базах данных (но не в общедоступной базе данных).
CKDatabase находится в CKContainer. Обычно для всего приложения существует только один CKContainer.
Краткое изложение концепций
Это показывает, как эти понятия соотносятся друг с другом:
Создание схемы CloudKit
На первый взгляд, структура данных для списка покупок будет аналогична приведенному выше примеру Book/Author с родительской записью «Shopping List» и набором дочерних записей «ShoppingItem»:
Именно так большинство руководств подходят к проблеме, и это похоже на то, как Shopping UK хранит данные внутри, но это не оптимальная структура для совместного использования набора элементов в списке.
Недостаточно представить только текущее состояние списка. Нам также нужно увидеть немного истории, чтобы мы могли ответить на такие вопросы, как:
- Я вчера добавил «молоко», но оно исчезло. Это глюк приложения или жена удалила его из списка?
- Я добавил «хлеб» на прошлой неделе, но он все еще в списке. Моя жена купила немного и они уже закончились?
- Когда мы в последний раз покупали кофе?
Поэтому вместо передачи снимка текущего списка Shopping UK передает упорядоченный список изменений, которые были применены к списку.
Этот «журнальный» подход похож на внутреннюю работу реляционной базы данных (см. ведение журнала с опережающей записью). Это хорошо зарекомендовавший себя шаблон, но он подходит не для всех приложений. Если у вас есть приложение, которому не нужно отслеживать каждое изменение, проще реализовать простую модель обмена на основе состояния, подобную той, что была показана ранее.
Когда все изменения будут применены к другому устройству, списки станут прежними.
Подход, основанный на журнале, имеет несколько полезных последствий:
- Элементы не имеют абсолютной позиции в списке. Вместо этого они знают, за каким пунктом они следуют. Это означает, что если мы с женой оба добавляем элементы в список, мы не перезапишем изменения друг друга. Вместо этого каждый элемент будет добавлен по очереди.
- Изменения можно ставить в очередь, а не применять немедленно. Это важно, когда приложение находится в режиме «Покупки». Было бы неприятно, если бы приложение добавляло элемент в мой контрольный список, пока я делал покупки в супермаркете. Это может привести к тому, что я вычеркну не тот элемент. Вместо этого приложение будет отображать недавно добавленные элементы в виде уведомлений, и я могу нажать на них, когда буду готов включить их в свой контрольный список:
Вам, наверное, интересно, что происходит после того, как список был опубликован в течение нескольких недель, и журнал содержит тысячи изменений. Мы рассмотрим, как сжимается журнал, в следующем посте.
Это третья часть из восьми частей, посвященных реализации обмена данными в Shopping UK с использованием CloudKit.
Часть 3: Добавление, обновление и удаление записей
Shopping UK — это умный список покупок для покупателей из Великобритании. Он знает почти каждый товар в супермаркете и расставит их по проходам. Списки могут быть разделены с семьей или друзьями.
На прошлой неделе мы рассмотрели концепции CloudKit и дизайн схемы Shopping UK. Сегодня мы рассмотрим, как CKRecords загружаются в iCloud с помощью CloudKit; и три способа, которыми приложение может получить CKRecords.
Добавление и изменение данных
Чтобы создать, изменить или удалить CKRecord в iCloud, приложение должно отправить сообщение CKModifyRecordsOperation в CloudKit.
Shopping UK использует CKModifyRecordsOperation для загрузки новых записей JournalEntry CKRecords в iCloud, чтобы они могли быть получены другими устройствами, имеющими общий список покупок.
Давайте посмотрим, что происходит, когда пользователь добавляет в список два новых элемента.
Шаг 1. Пользователь добавляет элементы в общий список
Пользователь может добавлять элементы в список тремя способами:
- Начните печатать и выберите вариант.
- Скопируйте элементы из другого списка.
- Вставка из другого приложения, например Notes, с помощью буфера обмена.
Неважно, как пользователь добавил «молоко» и «хлеб» в свой список покупок, способ их загрузки в iCloud через CloudKit одинаков.
Шаг 2. Приложение сохраняет изменения в локальном журнале
Shopping UK ведет местный журнал изменений. Все изменения сначала записываются в этот журнал, прежде чем приложение отправит их в iCloud.
Этот шаг не является обязательным для загрузки данных в iCloud с помощью CloudKit, но если ваше приложение должно гарантировать загрузку изменений, целесообразно сначала сохранить их на устройстве, даже если только временно, перед загрузкой изменений в iCloud.
Делая это, мы можем быть уверены, что ничего не будет потеряно, если сеть недоступна. Мы подробно рассмотрим, как что-то может выйти из строя, в части 8.
Шаг 3. Приложение отправляет CKRecords в iCloud с помощью CloudKit
Приложение создает сообщение CKModifyRecordsOperation и прикрепляет две записи CKRecord, представляющие изменения из локального журнала.
Примечание. Приложение может установить значение качества обслуживания (QoS) для CKModifyRecordsOperation. QoS более низкого уровня используется для операций, не являющихся критичными по времени. iOS может задерживать выполнение этих операций, когда устройство находится в режиме пониженного энергопотребления или при низком уровне заряда батареи.
Shopping UK использует уровень User-Initiated для отправки и получения записей JournalEntry. Это обеспечивает почти мгновенный обмен сообщениями (несколько секунд или меньше).
Шаг 4. CloudKit отправляет ответ обратно в приложение
CloudKit добавляет CKRecords в CKDatabase и возвращает три ответных сообщения.
Все операции CloudKit являются асинхронными и обычно следуют следующему шаблону:
- Приложение отправляет начальное сообщение в CloudKit
- CloudKit будет возвращать ответное сообщение для каждой завершенной единицы (т. е. два ответа, по одному для каждой CKRecord).
- CloudKit вернет еще одно ответное сообщение, когда вся операция будет завершена.
Здесь показан счастливый путь — когда все идет хорошо, — но из-за природы сетей и удаленных систем все может пойти не так. В части 8 мы вернемся к сценариям ошибок и к тому, как Shopping UK обрабатывает каждую из них.
Получение данных
Мы рассмотрели загрузку новых CKRecords в iCloud, но как данные извлекаются?
Есть три варианта:
Type | Operation to use |
Fetch By Name | CKFetchRecordsOperation |
Fetch By Query | CKQueryOperation |
Fetch Changes | CKFetchDatabaseChangesOperation CKFetchRecordZoneChangesOperation |
Shopping UK использует все три метода в разных местах.
Fetch By Name используется для:
- Получите запись CKShare, чтобы пользователь мог управлять общим списком (подробнее об этом в частях 4 и 7).
- Получить текущую запись ShoppingList для сжатия (подробнее об этом в части 7)
Fetch By Query используется для получения записей JournalEntry, подходящих для сжатия (подробнее об этом в части 7).
Fetch Changes — самая интересная из трех. Он использует CKServerChangeToken для идентификации каждой версии в истории базы данных, что означает, что приложение может запрашивать только те записи, которые изменились с момента его последнего запроса. Это намного эффективнее, чем каждый раз получать все подряд, и надежнее, чем создание собственного механизма для отслеживания изменений.
Для моей первой попытки реализовать совместное использование с помощью CloudKit, прежде чем я понял, как работает Fetch Changes, я попытался создать собственный механизм отслеживания изменений с использованием временных меток. Я потратил много времени на этот тупик, но он не работал должным образом из-за точности временных меток и крошечных различий в настройках часов каждого устройства.
Shopping UK использует Fetch Changes для получения изменений, сделанных другими устройствами, участвующими в общем списке.
Шаг 1. Приложение отправляет запрос на изменение базы данных в CloudKit.
Предположим, что две записи CKRecord уже существуют в iCloud, и это первый раз, когда белый iPhone запрашивает изменения.
Поскольку это первый раз, когда данные будут извлечены, у устройства не будет базы данных CKServerChangeToken, поэтому оно отправит значение null в качестве значения токена. Это указывает CloudKit вернуть все когда-либо сделанные изменения.
Шаг 2. CloudKit возвращает измененные зоны записи
CloudKit вернет три ответных сообщения:
- Ответ Change Token Updated, содержащий обновленную базу данных CKServerChangeToken (например, ABC123).
- Один ответ измененной зоны записи (поскольку обе записи CKRecord содержатся в одной CKRecordZone: abcd1234-…)
- Окончательный ответ «Изменения завершены»
Если несколько зон были изменены, для каждой зоны, содержащей изменение, будет ответ Changed Record Zone.
Если бы какие-либо зоны были удалены, также был бы возвращен дополнительный ответ Deleted Record Zone.
Шаг 3. Приложение сохраняет токен изменения базы данных
Маркер изменения базы данных сохраняется, чтобы его можно было использовать при следующей отправке CKFetchDatabaseChangesOperation.
Шаг 4. Приложение запрашивает изменения в каждой зоне
Теперь у нас есть список CKRecordZones, которые изменились, но мы пока не знаем, какие CKRecords. Чтобы найти их, приложение выдает CKFetchRecordZoneChangesOperation.
Поскольку это выдается впервые, у приложения еще нет Zone CKServerChangeToken, поэтому оно отправляет null.
Примечание. CKServerChangeToken базы данных, возвращенный на шаге 2, не совпадает с CKServerChangeToken зоны.
Шаг 5. CloudKit отвечает измененными записями
CloudKit будет отправлять отдельную запись «Запись изменена» для каждой записи, добавленной или измененной после Zone CKServerChangeToken. Поскольку эта операция используется впервые, а токен имеет значение null, будут возвращены все записи (молоко и хлеб).
В конце будет отправлен один ответ Fetch Completed.
Если запись была удалена с момента последней выборки, приложение также получит ответ Record Id Deleted.
Когда операция выборки будет завершена, приложение получит ответ «Выборка завершена». Он будет содержать Zone CKServerChangeToken.
Шаг 6. Приложение обновляет свой локальный список и сохраняет токен изменения зоны.
Приложение использует полученные CKRecords для обновления своего локального представления списка и обновляет пользовательский интерфейс новыми элементами.
Приложение сохраняет CKServerChangeToken для зоны «abcd1234-…» на устройстве. Это будет использоваться в следующий раз, когда CKFetchRecordZoneChangesOperation будет использоваться для получения изменений для той же зоны.
Механизм Fetch Changes в CloudKit обеспечивает эффективный способ синхронизации локального кеша приложения с данными iCloud. Вместо того, чтобы каждый раз получать все, нужно получать только последние изменения.
В части 6 мы рассмотрим, как может инициироваться запрос на получение изменений: когда приложение получает удаленное уведомление от подписки CloudKit или когда пользователь явно запускает синхронизацию списка.
А в части 7 мы рассмотрим ситуации, когда приложение должно получить все, например, когда пользователь входит в новую учетную запись iCloud.
Часть 4. Делимся концепциями и как начать делиться
Это четвертая статья из серии из восьми статей о реализации обмена данными в Shopping UK с помощью CloudKit.
Shopping UK — это умный список покупок для покупателей из Великобритании. Он знает почти каждый товар в супермаркете и расставит их по проходам. Списки могут быть разделены с семьей или друзьями.
На прошлой неделе мы рассмотрели три способа извлечения записей из iCloud, а также способы добавления и изменения данных. Сегодня мы рассмотрим, как работает общий доступ и как инициировать общий доступ к списку.
Где живут данные?
Прежде чем мы рассмотрим общий доступ, давайте рассмотрим, как данные разделяются в iCloud.
Каждая учетная запись iCloud видит три отдельные базы данных CKDatabase:
Когда приложение отправляет CKModifyRecordsOperation в iCloud для добавления данных, оно выбирает, какую из трех баз данных CKDatabase использовать.
Общедоступные и частные базы данных просты для понимания:
- Данные, добавленные в общий доступ, может просматривать или изменять любой (доступ может быть ограничен только пользователями, вошедшими в iCloud, или может быть сделан только для чтения для всех, кроме создателя)
- Данные, добавленные в Private, могут просматриваться и изменяться только владельцем учетной записи iCloud. Никто другой, даже такие разработчики приложений, как я, не может видеть вашу частную базу данных.
База данных Shared более интересна, потому что на самом деле она не содержит данных. Он просто раскрывает это. Думайте об этом как о представлении базы данных.
Что происходит, когда список является общим?
Есть три ключевых действия, связанных с обменом списком:
- Подготовка данных для обмена. Для Shopping UK данные включают элементы списка (например, «хлеб», «молоко») и атрибуты самого списка (название, цвет). Список уже будет существовать на устройстве «владельца», но его необходимо загрузить в iCloud, прежде чем он станет доступен для других людей.
- Отправка приглашения к совместному использованию. Приглашение будет отправлено с устройства владельца другим людям, которые станут «участниками» акции.
- Принятие приглашения. Другие участники должны открыть и явным образом принять общий ресурс, прежде чем можно будет обмениваться данными.
После настройки все изменения в общем списке будут синхронизированы между устройствами.
На следующей неделе, в части 5, мы увидим, как принимается приглашение к совместному использованию. Позже, в части 6, мы рассмотрим, как данные синхронизируются между устройствами.
Сегодня мы рассмотрим первые два действия: подготовка данных и отправка приглашения.
Шаг 1: Пользователь запрашивает общий доступ к списку
Совместное использование списка начинается, когда пользователь выбирает в меню опцию «Поделиться изменениями в списке»:
Шаг 2. Приложение проверяет состояние учетной записи iCloud и списка.
Перед обменом данными приложение проверяет:
- Доступна учетная запись iCloud.
- Список еще не опубликован.
Сообщение будет показано, если какая-либо проверка не пройдена.
Статус iCloud проверяется с помощью accountStatusWithCompletionHandler, доступного в CKContainer.
Приложение знает, что список является общим, если в записи локально кэшированного списка на устройстве задан идентификатор ShareId. Позже мы увидим, как это установлено.
Шаг 3. Приложение создает контроллер для управления действиями по обмену.
Приложение создает UICloudSharingController для управления взаимодействием с пользователем. Можно управлять взаимодействием с помощью пользовательского кода, но пользовательский интерфейс UICloudSharingController знаком, широко используется в собственных приложениях Apple и работает хорошо.
UICloudSharingControllerDelegate используется с UICloudSharingController для предоставления заголовка и эскиза изображения. Они показаны в левом верхнем углу листа обмена и в пригласительном сообщении, которое мы увидим на шаге 6.
Шаг 4: Подготовка обработчика
Подготовительный обработчик вызывается, когда пользователь выбирает способ отправки данных (например, сообщение, электронная почта, AirDrop, WhatsApp).
Подготовительный обработчик отвечает за добавление общих записей в iCloud.
Shopping UK сначала создает новую CKRecordZone в частной базе данных с помощью SaveRecordZone.
Новое имя CKRecordZone является производным от идентификатора списка (например, «List-db6a6e29-…»).
Поместив все записи для списка покупок в выделенную CKRecordZone, будет проще удалять данные атомарно. Мы вернемся к этому позже, в части 7, когда будем изучать, что происходит, когда пользователь перестает делиться списком или выходит из iCloud.
Затем CKRecordZone будет заполнена записями, представляющими список.
Шаг 5: Загрузите записи
После успешного создания CKRecordZone приложение отправляет CKModifyRecordsOperation в CloudKit:
В сообщение о загрузке включены только две записи CKRecord:
- Список покупок CKRecord. Он представляет собой сам список покупок, но не содержит никакой информации об имени, цвете или элементах списка. Они будут отправлены позже как JournalEntry CKRecords.
- CKShare. Это специальный тип CKRecord (тип записи: cloudkit.share), который идентифицирует CKRecords для предоставления.
CKShare действует как окно, через которое другие участники общего доступа могут просматривать общие записи. Есть два способа его использования:
- Поделиться CKRecordZone
- Поделитесь корневым CKRecord (и его дочерними элементами)
Вариант 1: поделитесь CKRecordZone
Если CKShare создается с помощью CKRecordZone, зона и ее содержимое будут общими.
Вариант 2: общий доступ к корневой записи CKRecord (и ее дочерним элементам)
Если CKShare создается с использованием CKRecord, эта корневая запись и ее дочерние элементы (и ее дочерние дочерние элементы и т. д. — вплоть до нижней части иерархии) будут общими.
В Shopping UK есть специальная CKRecordZone для каждого списка покупок, что означает, что доступны оба варианта. Но я решил предоставить общий доступ через корневую запись (вариант 2), а не по зоне (вариант 1), потому что я подумал, что в будущем может быть полезно иметь записи в зоне, которые не являются частью общего ресурса.
Помимо определения области видимости, CKShare также отвечает за регистрацию владельца общих записей и статуса принятия всех участников. «Владелец» — это учетная запись iCloud лица, инициировавшего общий доступ.
Что происходит с элементами списка?
Пока что мы загрузили заполнитель списка CKRecord и запись CKShare, но не загрузили самое главное — содержимое списка — «хлеб» и «молоко».
Я исключил элементы списка из начальной полезной нагрузки, потому что столкнулся с проблемами при совместном использовании очень больших списков (с несколькими сотнями элементов).
Я обнаружил, что загрузка займет много времени и вызовет тайм-аут. Об ошибках не сообщалось, но UICloudSharingController отказывался от создания общего ресурса. Это можно было воспроизвести последовательно, и контроллер закрывался примерно через 10 секунд.
Чтобы исправить это, я изменил стратегию, чтобы не загружать элементы заранее. Вместо этого элементы теперь добавляются во внутреннюю очередь загрузки, которая обрабатывается после завершения совместного использования (как часть шага 7 ниже).
Это имеет приятное последствие — более простую логику. Логика загрузки элементов списка после инициализации общего ресурса теперь такая же, как и логика загрузки элементов списка во время обычной синхронизации (которая будет обсуждаться позже, в части 6).
После загрузки данных приложение вызывает блок завершения PrepareHandler, чтобы сообщить UICloudSharingController о продолжении рабочего процесса приглашения.
Шаг 6: Отправка приглашения
После того, как блок завершения PrepareHandler получает от приложения сообщение об успешном выполнении, он запускает выбранный метод обмена (например, сообщения).
Пользователь выберет получателей сообщения обычным способом, добавит дополнительное содержимое сообщения, если захочет, и нажмет кнопку «Отправить».
Шаг 7: Завершение
После отправки сообщения с приглашением контроллер вызывает метод cloudSharingControllerDidSaveShare делегата.
Примечание. Я обнаружил, что метод cloudSharingControllerDidSaveShare иногда вызывается более одного раза. Это происходит, когда совместное использование инициируется несколько раз в одном и том же сеансе, как если бы обработчик событий подключался несколько раз и никогда не освобождался. Я не знаю, то ли это что-то, что я сделал неправильно, или ошибка в фреймворке (скорее всего, это было что-то, что я сделал), но мне пришлось обойти это, обеспечив идемпотентность всех операций.
Теперь данные в безопасности в iCloud, следующая задача приложения — связать список на устройстве со списком в iCloud.
Информацию об iCloud можно прочитать из свойства Share UICloudSharingController, которое содержит новый CKShare, созданный на шаге 5.
Для поиска данных iCloud достаточно следующих свойств:
CloudKitDatabase = "частный"
CloudKitZoneName = share.Id.ZoneId.ZoneName
CloudKitZoneOwner = share.Id.ZoneId.OwnerName
CloudKitShareId = доля.Id.RecordName
Наконец, пользовательский интерфейс обновляется, чтобы пользователь знал, что список является общим. Приложение также инициирует загрузку элементов JournalEntry («хлеб», «молоко») из внутренней очереди загрузки (как описано в шаге 5).
Теперь обмен завершен:
Часть 5. Принятие приглашения к совместному использованию
Это пятая часть из восьми частей, посвященных реализации обмена данными в Shopping UK с использованием CloudKit.
Shopping UK — это умный список покупок для покупателей из Великобритании. Он знает почти каждый товар в супермаркете и расставит их по проходам. Списки могут быть разделены с семьей или друзьями.
На прошлой неделе мы рассмотрели, как работает общий доступ и как начать делиться списком. Сегодня мы рассмотрим, что происходит, когда другое устройство принимает приглашение к совместной работе над общим списком.
Принятие share
Приглашение к совместному использованию отправляется от владельца одному или нескольким участникам, как правило, через сообщения или электронную почту. «Владелец» — это учетная запись, которая инициирует обмен.
Само приглашение — это просто ссылка на iCloud, вот так:
https://www.icloud.com/share/0123456789abcdefghijklmno#Shopping
Фрагмент (то есть часть после #) — это название акции, в данном случае «Покупки».
Шаг 1. Пользователь нажимает ссылку приглашения поделиться
Когда пользователь нажимает на ссылку-приглашение, iOS сначала проверяет, установлено ли приложение и подходит ли его версия для принятия акций.
На это указывает ключ CKSharingSupported в файле info.plist.
Перед выпуском версии 3.3 (первой версии Shopping UK с поддержкой совместного использования CloudKit) я выпустил версию 3.2, в которой был включен общий доступ. Идея заключалась в том, чтобы улучшить принятие совместного использования. Когда была опубликована версия 3.3, все существующие пользователи версии 3.2 могли сразу же принять общий доступ без необходимости предварительного обновления своего приложения.
После того, как система завершит проверку, пользователь увидит сообщение:
Шаг 2. Приложение информирует CloudKit о том, что общий ресурс принят
После того как пользователь принимает ссылку, iOS вызывает userDidAcceptCloudKitShareWith.
Объект CKShare.Metadata предоставляется в качестве параметра. Здесь есть все, что нужно приложению для поиска общих данных в iCloud.
Чтобы принять общий ресурс, Shopping UK отправляет сообщение CKAcceptSharesOperation указанному контейнеру CKContainer.
CloudKit изменит статус участника с Pending на Accepted.
Примечание. Прежде чем вызывать CKAcceptSharesOperation, Shopping UK проверяет, не добавлен ли список на устройство. Если он уже существует, список покупок открывается в приложении.
Шаг 3: Приложение создает новый список и назначает данные для обмена
Приложение создает новый пустой список на устройстве, используя информацию в CKShare.Metadata:
List Property | Copied From |
ListId | RootRecordId Name |
ListName | CKShare’s CKShareTitleKey |
ZoneId | RootRecordId ZoneId |
Database | “shared” |
Почему для базы данных установлено значение «Общий»?
Сначала это смутило меня, но вот что я узнал:
Когда Алиса делится списком, он загружается в ее частную базу данных CKDatabase.
Но когда Боб принимает приглашение присоединиться к списку Алисы, те же записи видны ему в его общей базе данных. Обе учетные записи могут вносить изменения в базовые данные через свою соответствующую базу данных: Алиса через свою личную базу данных, а Боб через свою общую базу данных.
Данные отображаются в другой базе данных в зависимости от того, является ли ваша учетная запись iCloud владельцем или участником.
Шаг 4. Приложение извлекает данные из iCloud и применяет их к устройству.
Затем приложение получает список изменений с помощью функции «Выборка изменений» (описанной в части 3). Но CKServerChangeToken игнорируется, чтобы обеспечить получение всех изменений.
По мере получения изменений приложение применяет их к новому списку, а затем показывает список пользователю.
Часть 6: Синхронизация данных
Это шестая часть из восьми частей, посвященных реализации обмена данными в Shopping UK с использованием CloudKit.
Shopping UK — это умный список покупок для покупателей из Великобритании. Он знает почти каждый товар в супермаркете и расставит их по проходам. Списки могут быть разделены с семьей или друзьями.
Мы рассмотрели, что происходит, когда другое устройство принимает приглашение к совместной работе над общим списком. Сегодня мы рассмотрим, как данные синхронизируются между устройствами, и некоторые проблемы, которые необходимо учитывать.
Что такое синхронизированный?
В Shopping UK нам нужно синхронизировать данные из двух типов записей:
- cloudkit.share. Это говорит нам, кто присоединился к общему списку или вышел из него.
- JournalEntry. Содержит все изменения, внесенные в список покупок.
Вот как они связаны:
ShoppingList представляет собой сам список, но с точки зрения синхронизации это очень скучно. cloudkit.share и JournalEntry — это место, где происходит действие.
Что особенного в cloudkit.share?
Cloudkit.share — это встроенный тип записи CloudKit. Он представляет объект CKShare. Это интересно, потому что содержит список тех, кто принял акцию.
Приложение не управляет списком участников напрямую (это работа UICloudSharingController (подробности см. в части 4 и части 5).
Однако приложение заинтересовано в отслеживании того, когда участник присоединяется к списку или покидает его, чтобы оно могло обновлять представление «Активность»:
Всякий раз, когда в CKRecord cloudkit.share обнаруживается изменение, приложение сравнивает список участников общего ресурса с ранее кэшированным списком, чтобы определить, что изменилось:
В этом примере Чарли и Диана присоединились к списку, а Боб ушел.
Этот список изменений участников используется для заполнения списка действий.
Что особенного в JournalEntry?
В целях совместного использования каждый список представляется в виде набора записей JournalEntry, как обсуждалось в части 2. JournalEntry можно использовать для представления каждого типа изменений, внесенных в список, и полного списка записей JournalEntry достаточно для восстановления всего списка. с нуля.
Например, этот список:
Представлен в виде семи записей JournalEntry CKRecords (новая запись вверху):
За восемь шагов мы можем увидеть, как эти семь записей JournalEntry применяются к пустому списку для воссоздания полного списка покупок:
После добавления JournalEntry в iCloud он никогда не будет изменен — это журнал изменений, доступный только для чтения. Единственный способ удаления записей JournalEntry — это когда приложение сжимает список, что будет происходить регулярно, чтобы список не стал большим и не вызывал проблем с производительностью. Сжатие списка будет подробно рассмотрено в части 7.
Каждая вновь созданная запись JournalEntry должна быть синхронизирована между каждым устройством и iCloud.
Как работает синхронизация?
В части 3 мы рассмотрели, как можно изменить данные в iCloud с помощью CKModifyRecordsOperation.
Мы также рассмотрели три способа чтения данных из iCloud:
- CKFetchRecordsOperation
- CKQueryOperation
- CKFetchDatabaseChangesOperation с CKFetchRecordZoneChangesOperation
Эти операции составляют основу синхронизации. Теперь давайте рассмотрим полный цикл и то, как каждое устройство узнает о внесении изменений.
Синхронизация, шаг 1. Загрузить изменения
Когда в список вносятся изменения на одном устройстве, запись, представляющая это изменение, должна быть загружена в iCloud как можно скорее.
Устройство, внесшее изменение, отправит запрос CKModifyRecordsOperation в iCloud с помощью CloudKit:
Синхронизация, шаг 2. Уведомление других устройств
После того как CloudKit обновит базу данных iCloud новой записью, он отправляет сообщение CKDatabaseNotification на все остальные устройства, на которых запущено приложение.
Синхронизация, шаг 3. Изменение запросов других устройств
После получения уведомления другое устройство выдаст CKFetchDatabaseChangesOperation, за которым следует CKFetchRecordZoneChangesOperation, чтобы запросить список всех изменений.
Шаг 4 синхронизации: iCloud отправляет изменения
iCloud отправляет список изменений, сделанных с момента последнего запроса. В этот список войдет только что созданная запись «Добавить картошку»:
Как iCloud узнает, какие устройства уведомлять?
Shopping UK использует подписки CloudKit, чтобы сообщать приложению, когда другой пользователь вносит изменения в общий список.
После того, как приложение зарегистрировало подписку в CloudKit, оно будет уведомлено об изменении отслеживаемых данных.
Как оформляются подписки?
Подписки управляются отдельно для каждого пользователя. Они не являются глобальными — они живут в учетной записи iCloud пользователя.
Существует три вида подписки:
Shopping UK использует CKDatabaseSubscription, которая используется для отслеживания изменений в конкретной базе данных CKDatabase и может быть дополнительно ограничена определенными типами записей.
Во время запуска приложения (willFinishLaunchingWithOptions) Shopping UK попытается зарегистрировать четыре подписки, по одной для каждого типа записи (cloudkit.share и JournalEntry) и по одной для каждой базы данных CKDatabase (частной и общей).
CKModifySubscriptionsOperation используется для регистрации четырех подписок:
- PrivateJournalEntryChanges
- Privatecloudkit.shareИзменения
- SharedJournalEntryChanges
- Sharedcloudkit.shareИзменения
Получаете уведомления от подписок?
Как только подписка будет зарегистрирована, iOS вызовет метод didReceiveRemoteNotification AppDelegate, чтобы сообщить приложению, когда произойдет изменение.
Это уведомление может быть получено, когда приложение активно, в фоновом режиме или даже когда оно не запущено. (источник)
система вызывает этот метод, когда ваше приложение работает на переднем плане или в фоновом режиме. Кроме того, если вы включили фоновый режим удаленных уведомлений, система запускает ваше приложение (или выводит его из приостановленного состояния) и переводит его в фоновое состояние при поступлении удаленного уведомления.
Основы:
- Вызов обработчика завершения после обработки уведомления.
- Вернуть соответствующий результат: NewData, NoData или Failed. Будьте последовательны в том, как вы их используете.
- У вас есть только 30 секунд, чтобы обработать уведомление. Будьте максимально быстрыми и энергоэффективными.
Важно: уведомление не всегда будет получено
Отправка уведомления из iCloud не гарантируется. iOS использует эвристику для принятия решения о том, когда отправлять уведомление.
Apple не документирует эвристики. Предположительно, потому что он не хочет, чтобы разработчики играли в систему. Лучшим официальным документом, который я нашел, был Apple TechNote TN2265 от 2016 года:
ваше приложение получит уведомление, если iOS или OS X решит, что это энергоэффективно. Если бюджет энергии или данных для устройства превышен, ваше приложение больше не будет получать уведомления с ключом доступности содержимого, пока бюджет не будет сброшен. Это происходит один раз в день и не может быть изменено действиями пользователя или разработчика.
iOS отслеживает, сколько времени требуется вашему приложению для обработки каждого уведомления и какой результат был возвращен вашим приложением.
Пример:
iOS использует эти данные, чтобы решить, как обрабатывать следующее уведомление. Должен ли он вообще отправляться в приложение? И если он будет признан достойным, как скоро его следует отправить?
Мое предположение, основанное на большом количестве тестов, заключается в том, что iOS группирует эти результаты по типу, а затем вычисляет статистику по каждой группе:
- Среднее время для newdata было 2750 мс
- Среднее время для nodata было 60 мс
iOS, похоже, использует эту информацию и другие данные (заряд батареи, скорость разрядки батареи, количество полученных уведомлений), чтобы решить, какой бюджет выделить вашему приложению на будущие уведомления.
Во время тестирования я усвоил пару важных уроков.
Урок 1: Всегда возвращайте результат
Я обнаружил ошибку в своем приложении, из-за которой результат не возвращался, когда приложение работало в фоновом режиме. Когда это происходило, CloudKit отказывался отправлять уведомления в течение нескольких минут. Мое приложение не играло по правилам, поэтому его наказывали.
Урок 2. Правильно классифицируйте результаты
Другая ошибка означала, что мое приложение иногда возвращало nodata вместо newdata, несмотря на то, что на самом деле извлекало и обрабатывало подлинные данные. Это произойдет примерно в 50% случаев. Я добавлял элемент в список на одном устройстве, но уведомление не появлялось на втором устройстве. Я бы добавил второй элемент, и на этот раз уведомление было бы получено на втором устройстве.
Поскольку приложение неправильно сообщало о работе как nodata, iOS уменьшила количество отправляемых уведомлений вдвое. С точки зрения iOS это имело смысл — зачем тратить ресурсы на уведомления, которые занимают столько же времени для обработки, но не производят полезной работы.
Все дело в экономии батареи. Если приложение злоупотребляет временем, отведенным для обработки удаленных уведомлений, iOS в будущем предоставит ему меньше времени. Если приложение вообще не отвечает, iOS накажет приложение. Если приложение говорит, что оно не сделало ничего полезного за отведенное ему время, iOS предоставит ему меньше времени в будущем. Это может привести к пропуску, объединению или задержке уведомлений.
Лучшая стратегия — эффективно использовать ресурсы — не делать больше, чем нужно, когда приходит уведомление, — и быть правдивым в том, что вы сделали. Если ваше приложение использовало свое время для получения новых данных, верните newdata. Если вашему приложению ничего не нужно делать, верните nodata. И если ваше приложение не может завершить обработку по какой-либо причине, возврат не выполнен. Это позволит iOS классифицировать время, затраченное на правильный набор данных, чтобы сделать правильные выводы.
И последний совет: при тестировании, если ваши уведомления приходят на одни устройства, но не приходят на другие, попробуйте:
- Зарядка устройства. iOS будет стараться экономить заряд батареи, когда она разряжена, и пропуск уведомлений — один из способов сделать это.
- Отключить «Не беспокоить». Я видел ситуацию, когда это препятствовало приходу уведомлений. Как только я отключил «Не беспокоить», уведомления снова стали приходить.
- Подожди некоторое время. iOS, кажется, сбрасывает свою статистику через некоторое время. Попробуйте подождать несколько минут или даже несколько часов. Или попробуйте другое устройство.
Как приложение реагирует на уведомления о подписке?
После получения уведомления Shopping UK извлекает список изменений с помощью CKFetchDatabaseChangesOperation и CKFetchRecordZoneChangesOperation, как описано в шаге 3 выше и в части 3 этой серии.
Может возникнуть соблазн пропустить шаг «выборки» и использовать информацию, предоставленную в уведомлении, но это ненадежно. iOS может принять решение об объединении или удалении уведомлений об отдельных изменениях, поэтому требуется явная выборка, чтобы гарантировать, что никакие изменения не будут пропущены.
Как обрабатывается список изменений?
После того, как приложение сделает запрос, CloudKit отправит несколько ответов, которые могут включать:
- набор CKRecords
- список удаленных RecordZone.ID,
- список удаленных Record.ID
Подробную информацию см. в части 3.
Когда приложение получает изменения, оно сохраняет их локально до тех пор, пока не придет время применить их к пользовательскому интерфейсу.
«Правильное время» зависит от того, какой список активен, и от того, что делает пользователь.
Когда приложение находится на экране «Планирование», операции JournalEntry можно сразу применить к текущему списку. Но, если приложение находится на экране «Покупки», нужен другой подход. Если бы элементы были добавлены в видимый список немедленно, это нарушило бы поток пользователя (представьте, если бы набор новых элементов был добавлен в ваш список как раз в тот момент, когда вы собирались его пометить. Было бы неприятно случайно нажать не тот элемент, потому что приложение решило перетасовать элементы списка как раз в этот момент).
Чтобы обеспечить лучший пользовательский интерфейс, Shopping UK отображает каждое «Добавить» в виде видимого уведомления в верхней части списка. Затем, когда пользователь коснется уведомления, элементы в очереди переместятся в нужную категорию в списке.
Когда элемент «Отмечен» или «Удален», приложение обрабатывает вещи по-другому. Поскольку элемент уже есть в списке, нет необходимости отображать отдельное уведомление. Вместо этого ячейка таблицы элемента обновляется, чтобы отразить изменение состояния.
Такое поведение делает использование Shopping UK удовольствием, когда семья и друзья делают покупки вместе. Некоторым людям нравится разделяться в супермаркете, чтобы сэкономить время. Чтобы это работало, очень важно, чтобы изменения появлялись быстро и были визуально очевидны, чтобы один и тот же товар не покупался дважды.
Когда синхронизировать?
В большинстве случаев синхронизация происходит автоматически. Если пользователь вносит изменения в список, запись JournalEntry, представляющая это изменение, будет немедленно загружена в iCloud. Если получено уведомление CKDatabaseNotification, указывающее на то, что кто-то еще внес изменения, приложение немедленно получит изменения из iCloud.
Кроме того, приложение будет загружаться при запуске приложения (в случае, если другие участники внесли какие-либо изменения, пока приложение находилось в фоновом режиме).
Но этого не всегда достаточно.
Чтобы справляться с непредвиденными ситуациями, такими как подписки CloudKit, которые не приходят вовремя, я также добавил способ запуска полного процесса загрузки и извлечения вручную.
Это можно вызвать, потянув вниз экран «Планирование» (показан ниже) или экран «Покупки» (тот, что с флажками).
Еще один способ вызвать ручную синхронизацию — нажать кнопку «Синхронизировать сейчас» на листе активности:
Обычно я не люблю добавлять несколько способов сделать одно и то же — я бы предпочел, чтобы был один надежный способ выполнить что-то — но когда есть много движущихся частей, некоторые из которых находятся вне моего контроля (например, iCloud , подписки на базы данных и сеть), приятно знать, что у пользователей есть ручная альтернатива, доступная при необходимости.
Часть 7. Управление общим ресурсом и фоновое обслуживание
Это седьмая часть из восьми частей, посвященных реализации обмена данными в Shopping UK с использованием CloudKit.
Shopping UK — это умный список покупок для покупателей из Великобритании. Он знает почти каждый товар в супермаркете и расставит их по проходам. Списки могут быть разделены с семьей или друзьями.
На прошлой неделе мы рассмотрели, как данные синхронизируются между устройствами, и некоторые проблемы, которые необходимо учитывать. Сегодня мы рассмотрим, как изменить или остановить общий ресурс, что происходит при удалении списка и действиях по фоновому обслуживанию.
Управление долей в качестве владельца
«Владелец» списка — это человек, который изначально поделился им.
Из части 4 вы помните, как UICloudSharingController используется для создания начального общего ресурса. Этот же контроллер также используется для просмотра и управления общим ресурсом после его создания.
Как владелец списка, вы можете:
- Посмотреть список участников.
- Пригласите новых участников.
- Удалить участников.
- Прекратите делиться списком.
Приглашение новых участников
Только владелец списка может приглашать новых участников.
Вы можете пригласить до 100 человек. Это ограничение наложено CloudKit, но для наших целей его более чем достаточно. UICloudSharingController имеет параметры для управления тем, кто может принять приглашение (любой со ссылкой или именованными учетными записями), и их разрешениями на доступ (чтение-запись или только чтение).
Списки в Shopping UK могут быть доступны только для именованных учетных записей и разрешений на чтение и запись — пользователь не может выбирать. Это может измениться в будущем, но я, честно говоря, не мог придумать вариант использования для разрешения списка покупок только для чтения или анонимного доступа в настоящее время (возможно, у вас есть такой?).
Приложению не нужно делать ничего особенного, когда пользователь добавляет нового участника. Обо всем позаботится UICloudSharingController:
- Создание и отправка приглашений
- Обновление записи cloudkit.share с данными нового участника
- Обновление статуса принятия cloudkit.share
Когда статус cloudkit.share изменится на «принято», CKRecordZone для общего списка автоматически появится в общей базе данных CKDatabase участника.
Удаление участников
Удалить участника может только владелец списка, но любой может удалить себя.
Как владелец, вы удаляете участника с помощью кнопки «Удалить доступ»:
Примечание. Если в списке только один участник, общий доступ к списку прекратится.
Приложению не нужно делать ничего особенного для поддержки этого действия. UICloudSharingController позаботится обо всем — например, об обновлении записи cloudkit.share для удаления сведений об участнике, что скроет CKRecordZone списка из общей базы данных CKDatabase участника.
Удаленный участник увидит это сообщение до того, как список будет удален с его устройства:
Это обрабатывается подписками и CKFetchDatabaseChangesOperation, описанными в частях 3 и 6. Операция выборки возвращает ответ Deleted Record Zone, и приложение отвечает, показывая это сообщение.
Примечание. Данные списка по-прежнему находятся в iCloud, в частной базе данных владельца и на устройствах других участников.
Прекратить делиться
Кнопка «Остановить общий доступ» отключит общий доступ для всех участников и удалит данные из iCloud.
Когда используется этот параметр, приложению необходимо выполнить некоторые собственные действия, чтобы сбросить локальный список, чтобы он стал только локальным списком, как это было до того, как список был впервые опубликован.
Приложение должно делать три вещи:
- Удалите локально сохраненные CKServerChangeTokens
- Удалите CKRecordZone, содержащую данные списка, из iCloud.
- Удалите локальную ссылку на список iCloud.
Приложение уведомляется о действии «Прекратить общий доступ» путем реализации метода cloudSharingControllerDidStopSharing в UICloudSharingControllerDelegate.
Зона удаляется с помощью deleteRecordZoneWithID в базе данных CKDatabase.
При удалении зоны каждый участник получит сообщение, похожее на то, которое появляется при удалении одного участника.
Управление долей в качестве участника
Как участник, UICloudSharingController показывает другое представление. Нет возможности пригласить других, остановить общий доступ или удалить других участников. Единственный вариант — удалить себя из списка:
UICloudSharingController заботится об этом механизме, обновляя запись cloudkit.share.
Что происходит при удалении списка
Общий список может быть удален как владельцем, так и участником. Поведение у всех разное.
Удаление списка как владельца
Когда владелец удаляет список, удаляется все:
- Список данных с устройства владельца.
- CKRecordZone списка из частной базы данных CKDatabase владельца в iCloud.
- CKRecordZone списка из общей базы данных CKDatabase всех участников в iCloud.
- Список данных с устройств всех участников.
Приложение владельца выполняет задачи 1 и 2, используя логику «Остановить общий доступ» для удаления зоны.
Задача 3 выполняется автоматически, потому что при удалении частной зоны владельца общие зоны тоже исчезают.
Приложение участника обрабатывает задачу 4 так же, как и для «Прекратить делиться».
Удаление списка в качестве участника
Когда участник удаляет список, все проще, потому что исходный список не удаляется — он остается и в iCloud, и на устройстве владельца, и на устройствах других участников.
Убрать нужно всего две вещи:
- Список данных с устройства участника.
- Cloudkit.share CKRecord из общей базы данных CKDatabase участника в iCloud.
Приложение справляется с обеими задачами самостоятельно. Cloudkit.share удаляется, как и любой другой CKRecord, с помощью deleteRecordWithID, который обеспечивает простую оболочку вокруг CKModifyRecordsOperation.
Примечание: после удаления списка участник может присоединиться позже, используя то же самое приглашение, которое он первоначально получил.
Что происходит, когда пользователь входит в iCloud или выходит из него?
Важно понимать, что когда список впервые публикуется, список покупок больше не считается локально размещенным списком, который находится на устройстве. Теперь это список, размещенный в iCloud, а данные на локальном устройстве — это просто локально кэшированная копия.
Это означает, что когда пользователь выходит из iCloud, все списки, размещенные в iCloud, должны быть удалены с локального устройства. Если бы приложение этого не делало, у него не было бы возможности гарантировать их целостность, потому что у пользователя больше нет доступа к базе данных iCloud, содержащей изменения списка. Кроме того, разрешение постоянного доступа к списку нарушит безопасность списка, поскольку пользователь вышел из iCloud и больше не имеет права просматривать содержимое списка.
Когда пользователь входит в iCloud, происходит обратное. Приложение восстановит копию всех размещенных в iCloud списков из частной и общей баз данных новой доступной учетной записи iCloud.
Приложение обнаруживает изменение состояния учетной записи, наблюдая за уведомлением CkAccountChanged.
Когда это уведомление срабатывает, приложение отправляет сообщение accountStatus в CloudKit, чтобы проверить состояние учетной записи.
Если ответ указывает на изменение состояния с «Нет учетной записи» на «Доступно», все списки, размещенные в iCloud, восстанавливаются на устройстве.
Если ответ указывает на изменение статуса с «Доступно» на «Нет учетной записи», все списки, размещенные в iCloud, удаляются с устройства.
Уведомления об изменении учетной записи до сих пор были надежными. Но мне нравится иметь ручной вариант, если что-то пойдет не так. Вот почему я добавил параметр для принудительного повторного получения всех списков, размещенных в iCloud, в разделе «Устранение неполадок» меню «Справка».
Без этой опции пользователю пришлось бы выйти, а затем снова войти в iCloud для обновления, а это может занять много времени.
Фоновое обслуживание
После того, как общий ресурс установлен, пользователю не нужно ничего делать. Приложение будет счастливо синхронизировать изменения в течение всего дня.
Но за кулисами ничто не стоит на месте.
Если вы следили за предыдущими сообщениями, вы помните, что Shopping UK не синхронизирует состояние списка. Вместо этого он синхронизирует записи журнала, представляющие историю изменений, внесенных в список (см. часть 2 и часть 6 для освежения знаний). И из этих изменений можно построить текущий список.
Но вам, наверное, интересно, что произойдет через несколько месяцев, когда будут тысячи записей JournalEntry?
Во-первых, для устройств, которые уже совместно используют список, не имеет значения, сколько существует записей JournalEntry, потому что, как мы видели в части 3 и части 6, приложение использует CKServerChangeTokens только для получения новых записей.
Но это не отпускает нас полностью.
Что произойдет, если журнал продолжит расти?
Две проблемы:
- Хранилище и квоты.
- Новые участники присоединяются к списку.
Каждый CKRecord занимает место (хотя и всего несколько байтов), и это учитывается в квоте хранилища iCloud владельца общего ресурса. Если бы количество записей JournalEntry было разрешено неограниченно расти, хранилище iCloud владельца заполнилось бы.
Во-вторых, если новый участник присоединится к списку позже, размер журнала станет актуальным. Помните, как в части 5 CKServerChangeToken не используется при первоначальном получении списка. При большом журнале получение данных из iCloud и применение изменений к представлению в пользовательском интерфейсе может занять значительное время.
Ни одна из этих ситуаций не является хорошей.
Каков ответ?
Подход, который я выбрал для Shopping UK, заключался в том, чтобы сжать журнал, удалив старые записи JournalEntry, которые не нужны для восстановления текущего списка.
Например, два приведенных ниже списка идентичны, но второй был создан с использованием меньшего количества записей JournalEntry:
Правда, удалив строки 1 и 3, мы потеряли часть истории списка, но финальное состояние обоих списков одинаково.
Давайте посмотрим на другой пример. Эти два списка эквивалентны?
Нет, не совсем. Первый список включает «хлеб» в выделенном состоянии, а второй «хлеб» вообще не показывает.
Имеет ли это значение? Возможно, нет.
Если «хлеб» куплен давно, то должен ли он быть в списке? В конце концов, цель списка — рассказать нам, что нам нужно купить, а не то, что было куплено.
Опять же, если моя жена только что купила «хлеб», мне было бы полезно видеть его вычеркнутым из списка, чтобы я знал, что он был куплен, а не просто удален.
Из этих примеров мы можем вывести некоторые общие принципы:
- Недавние элементы следует оставить в покое, а
- Полная история изменений должна сохраняться в представлении «Активность».
Вот как работает сжатие журнала в Shopping UK.
Сжатие никогда не применяется ко всему набору изменений JournalEntry, а только к тем, которые были созданы до установленного времени.
И каждое изменение, внесенное в список, также записывается в представлении «Активность», которое не будет затронуто при сжатии журнала.
Когда записи JournalEntry сжимаются?
Сжатие происходит в двух местах:
- Перед обновлением пользовательского интерфейса
- В iCloud через равные промежутки времени
Перед обновлением пользовательского интерфейса
Когда устройство запрашивает новые изменения из iCloud, может быть возвращено много записей JournalEntry. Быстрое обновление пользовательского интерфейса с таким большим количеством изменений может замедлить взаимодействие и отвлечь пользователя. Чтобы предотвратить это, приложение будет сжимать записи JournalEntry старше трех часов и применять только оставшиеся изменения к пользовательскому интерфейсу.
На практике это означает, что если пользователь на другом устройстве добавил и отменил «молоко» более 3 часов назад, оно не будет отображаться в списке (поскольку и «добавление», и «отметка» произошли более 3 часов назад). несколько часов назад). Но если пользователь добавил и пометил «молоко» в течение 3 часов, элемент будет отображаться в списке в таком же помеченном состоянии, как здесь: молоко
В iCloud
Журнал в iCloud также будет регулярно сжиматься.
Каждое устройство, имеющее доступ к общему списку, отвечает за сжатие записей JournalEntry в iCloud. После каждой загрузки в iCloud (см. Часть 6) приложение проверяет, можно ли сжать журнал. Но для экономии заряда батареи и пропускной способности это будет происходить не чаще, чем каждые 30 минут.
Сжатие представляет собой трехэтапный процесс:
- Получить подходящие записи JournalEntry из iCloud.
- Попробуйте сжать их в приложении.
- Удалите удаленные записи JournalEntry из iCloud и обновите дату CompressedUntil в записи ShoppingList.
Записи JournalEntry старше 14 дней считаются подходящими для сжатия.
Приложение использует CKQueryOperation для получения подходящих записей JournalEntry.
Если логика сжатия идентифицирует записи, подлежащие удалению, CKModifyRecordsOperation используется для удаления записей из iCloud и обновления даты CompressedUntil.
По умолчанию используется SavePolicy IfServerRecordUnchanged, что означает, что вся операция завершится успешно или завершится атомарно.
Последствия сжатия
Сжатие журнала — это хозяйственная деятельность, и пользователи должны в основном не обращать внимания на его существование. Единственный раз, когда эффект сжатия будет виден пользователям, это если их устройство не использовалось более 14 дней. Если это произойдет, приложение выполнит полную повторную выборку общих списков из CloudKit, чтобы убедиться, что они полностью обновлены.
Между прочим, если вы использовали приложение до того, как была выпущена версия 3.3, вы могли столкнуться с проблемой, которая доставила мне бесконечный стресс, пытаясь воспроизвести и диагностировать. Старый доморощенный механизм обмена использовал примитивную форму ведения журнала и не имел способа определить, не обновлялось ли устройство в последнее время. Разочарованные пользователи присылали мне сообщения с вопросом, почему синхронизация перестала работать после того, как приложение какое-то время не использовалось. Если вы были одним из пользователей, затронутых этой проблемой, мне очень жаль, что мне потребовалось так много времени, чтобы найти и исправить.
Альтернативы сжатию
Сжатие журнала — это удобный способ уменьшить размер журнала упреждающей записи приложения. Это хорошо работает для списка покупок, потому что состояние элементов в списке следует предсказуемой схеме: они добавляются, затем отмечены или удалены. И каждую пару начальных и конечных состояний (Добавить, затем Отметить) и (Добавить, затем Удалить) можно аккуратно очистить, не затрагивая другие элементы. Для других типов приложений более подходящая стратегия очистки для журнала с упреждающей записью может включать в себя какую-то отметку нижнего предела.
Часть 8: Когда что-то идет не так
Это последняя статья из серии из восьми статей о реализации обмена данными в Shopping UK с помощью CloudKit.
Shopping UK — это умный список покупок для покупателей из Великобритании. Он знает почти каждый товар в супермаркете и расставит их по проходам. Списки могут быть разделены с семьей или друзьями.
Ранее мы рассмотрели, как изменить или остановить общий доступ, что происходит при удалении списка и фоновом обслуживании. Сегодня мы завершим серию обзором обработки ошибок, слияния данных и диагностики проблем.
Путь печали
На протяжении всей этой серии основное внимание уделялось счастливому пути — когда все идет по плану — но решение для синхронизации хорошо настолько, насколько хорошо оно может справляться с непредвиденными ситуациями.
Мы начнем с рассмотрения того, что может пойти не так, прежде чем изучать, как с этим справляется Shopping UK.
Я не могу гарантировать, что продумал все возможные проблемные сценарии, но я поделюсь тем, что я создал, чему я научился из этого, и некоторыми методами, которые я использовал для диагностики проблем, когда они действительно возникают.
Почему что-то идет не так?
Синхронизация данных затруднена по двум причинам:
- Объединение данных: данные должны быть объединены из разных источников — между устройством каждого пользователя и iCloud — при сохранении целостности данных и стремлении обеспечить согласованность данных на всех устройствах.
- Сеть: данные должны перемещаться между физически разделенными системами — устройством пользователя и iCloud — по ненадежной сети.
Проблема с сетями
CloudKit предоставляет чистый интерфейс для перемещения данных между устройством и iCloud, но не может изменить природу сетей:
- Сеть никогда не бывает надежной.
- Задержка никогда не равна нулю.
- Пропускная способность никогда не бывает бесконечной.
- Транспортные расходы никогда не равны нулю.
- Сеть никогда не бывает однородной.
Каждый запрос, отправленный из вашего приложения в iCloud, должен передаваться с устройства пользователя в iCloud по многим сетям. Каждое сообщение будет начинаться с Wi-Fi или мобильной (сотовой) связи, а затем находить более крупные интернет-магистрали и в конечном итоге поступать в центр обработки данных, в котором размещены серверы Apple iCloud. Путь к iCloud будет разным для каждого пользователя и, возможно, для каждого сообщения.
Wi-Fi не всегда будет доступен, мобильные (сотовые) данные могут быть отключены (например, включен режим полета или отключен роуминг данных) или иметь неоднородный прием, iCloud может быть недоступен или занят, а сообщения могут быть потеряны.
Ничего не происходит мгновенно. Доставка каждого сообщения занимает несколько миллисекунд или больше — это в миллионы раз медленнее, чем сообщение может быть обработано на устройстве. Одновременно может находиться много сообщений. Одновременно может прийти много ответов.
У всего есть цена. Сообщения большего размера требуют большей обработки, занимают больше места в хранилище iCloud и потребляют больше мобильного (сотового) тарифного плана пользователя.
У каждого будет разный опыт. Некоторые пользователи имеют сверхбыстрый широкополосный доступ, некоторые ограничены слабым или медленным сигналом данных. Некоторые части мира ближе к центру обработки данных, чем другие.
Проблема с объединением данных
Цель решения для синхронизации — перемещать данные между устройствами, чтобы предоставить каждому пользователю одинаковую актуальную картину мира.
Но сеть не всегда будет доступна, сообщениям требуется время, чтобы перемещаться между устройствами, и пользователи ожидают, что их приложение будет продолжать работать, когда их мобильный сигнал неоднороден. Лучшее, на что мы можем надеяться, — это на то, что все устройства в конечном итоге будут отображать единообразное представление данных.
А это значит, что мы должны подумать, что делать, когда одни и те же данные меняются двумя людьми одновременно.
Как это решить?
- Должны ли изменения применяться по порядку?
- Должны ли они быть как-то объединены?
- Должны ли они оба быть отвергнуты?
На этот вопрос нет «правильного» ответа — все зависит от характера приложения, значения данных и ожиданий пользователей.
Приручение сети
При разработке стратегии обмена для Shopping UK я руководствовался несколькими руководящими принципами:
- Синхронизируйте изменения с другими как можно скорее.
- Не теряйте никаких изменений.
- Минимизируйте затраты пользователя на передачу данных.
- Эффективно используйте аккумулятор.
Это не универсальные принципы, которые работают для всех приложений. Если ваше приложение передает информацию о местоположении пользователя в режиме реального времени, потеря одного обновления может быть допустима. Но потерянные вещи — не лучший вариант для приложения со списком покупок.
Поскольку сеть ненадежна, я выбрал решение на основе очередей.
Когда пользователь добавляет элемент, приложение создает запись JournalEntry для представления изменения. Это сохраняется в локальной очереди, а затем очередь загружается в iCloud (с помощью CloudKit).
Если сеть выходит из строя, ничего не теряется.
То же самое происходит при получении изменений из iCloud. Вновь полученные записи JournalEntry сохраняются в локальной очереди, и очередь обрабатывается, и каждое изменение применяется к списку по очереди.
После загрузки локального изменения оно удаляется из очереди загрузки.
После применения удаленного изменения к устройству запись удаляется из очереди на применение.
Загрузка может завершиться неудачно по многим причинам. Полную информацию см. в ошибках CloudKit.
Что делает приложение, когда оно получает сообщение об ошибке?
Как правило, Shopping UK просто показывает удобную форму ошибки в журнале действий, чтобы пользователь знал, что синхронизация может быть отложена.
Например, когда режим полета включен, будет возвращено значение NetworkUnavailable:
Это то, что происходит со всеми ошибками?
Нет, просто ошибки, которые не могут быть исправлены приложением.
CloudKit защищает себя от огромных сообщений.
- Если запрос слишком велик или содержит слишком много записей, CloudKit отклонит его с ошибкой LimitExceeded.
- Если запросы выполняются слишком быстро, CloudKit может отклонить некоторые из них с ошибкой RequestRateLimited. Я также иногда видел ошибку PartialFailure с сообщением «не будет сохранено, но можно повторить как есть». Это происходит, когда iCloud находится под большой нагрузкой.
Приложение знает, что делать с этим типом ошибки:
- Когда запрос содержит слишком много записей, Shopping UK делит список пополам и отправляет каждую половину отдельно. Если какая-либо половина терпит неудачу, она снова делится пополам. Это деление пополам продолжается до тех пор, пока все не будет успешно отправлено.
- Когда запрос отклонен из-за ограничения скорости или когда возвращается сообщение «не будет сохранено, но может быть повторено как есть», Shopping UK подождет некоторое время и повторит попытку.
Укрощение слияния
Всегда ли необходимо слияние?
Нет. Этого можно избежать, никогда ничего не кэшируя на устройстве и рассматривая iCloud как единственный авторитетный источник:
- Когда вашему приложению нужны данные, оно извлекает их из iCloud.
- Чтобы изменить данные, ваше приложение немедленно загружает изменения в iCloud.
- Если загрузка не удалась из-за того, что в то же время изменения были внесены другим устройством, извлекается свежая копия, и пользователь снова вносит изменения.
Именно так веб-сайты работали в самые первые дни.
Это может быть нормально для некоторых типов приложений, но не для списка покупок. Если вы находитесь в сельском магазине без сигнала данных, было бы неприятно, если бы ваш список покупок был в автономном режиме и не мог быть просмотрен.
Итак, мы застряли в реальности, что данные на устройстве и данные в iCloud могут расходиться. А это значит, что слияния необходимы.
Когда мы объединимся?
Прелесть использования журнала только для записи в том, что при загрузке изменений в iCloud слияния никогда не требуются. Каждое изменение просто добавляется к набору записей JournalEntry в iCloud.
Но это не устраняет слияние, а просто перемещает его в другое место! Вот почему я люблю дизайн. Нет ничего бесплатного; все дело в обмене 🙂
Для Shopping UK слияние происходит, когда нам нужно применить изменения к устройству пользователя.
Вот несколько примеров.
Добавление в автономном режиме
Алиса и Боб оба не в сети, телефон не принимается.
Алиса добавляет «хлеб» под «молоко» за мгновение до того, как Боб добавляет «яйца» под «молоко».
Что произойдет, когда их сеть вернется — как мы разрешим этот конфликт?
Shopping UK применит изменения по порядку. Это может привести к тому, что списки Алисы и Боба будут отображаться в несколько ином порядке:
Помните, что это необычное событие. Нечасто оба пользователя добавляют элементы в свой общий список, когда оба находятся в автономном режиме. И хотя у каждого пользователя свой порядок, это не повлияет на использование списков. Вещи никогда не теряются, и когда Алиса и Боб начнут делать покупки, их списки будут автоматически распределены по категориям и упорядочены в соответствии с проходами в супермаркете.
Можно привести аргументы в пользу лучшего подхода, но, на мой взгляд, это разумный компромисс, преимущество которого заключается в том, что приложение можно продолжать использовать в автономном режиме.
Переименование или изменение цвета списка в автономном режиме
Некоторые ситуации слияния решить намного проще.
Если бы два пользователя изменили цвет или название своего общего списка покупок в автономном режиме, Shopping UK просто применил бы изменения в том порядке, в котором они были внесены — последний из них выигрывает.
Например, Алиса и Боб оба не в сети. Алиса меняет название списка на «Еженедельный магазин» за секунду до того, как Боб переименует список в «Список Алисы и Боба». Когда устройства снова подключатся к сети, каждое устройство загрузит изменение «переименовать», и оба устройства получат «переименование» другого человека. На обоих устройствах операции будут применяться в одинаковом порядке. Это приведет к тому, что список будет называться «Список Алисы и Боба», потому что Боб сделал свое изменение после Алисы.
Легко диагностировать проблемы
Когда я создавал решение для синхронизации, часто что-то шло не так — из-за моего непонимания, ошибок или недокументированного поведения iCloud.
Диагностировать причину проблемы не всегда было легко.
Вот некоторые вещи, которые помогли мне.
Вход в консоль
Наличие подробных журналов было необходимо для диагностики проблем. Многие ошибки невозможно найти, просто зафиксировав текущее состояние системы. Часто бывает необходимо увидеть, как состояние менялось с течением времени. Большинство вызовов CloudKit асинхронны, и это усложняет работу.
При написании кода я везде добавлял сообщения «запись в журнал» — почти для каждого пути по коду. Во многих случаях эти сообщения журнала стали заменой комментариев кода.
Моя структура ведения журнала имеет настраиваемую детализацию. Во время разработки я установил его в режим DEBUG, чтобы отображать каждое сообщение в консоли. Это означало, что я мог точно видеть, что происходит внутри, и в режиме реального времени. Журналы показывали каждое решение, содержание каждого запроса, содержание каждого ответа, сообщения об ошибках, все.
Для версии, выпущенной в App Store, регистрируются только сообщения об ОШИБКАХ и критическая информация. Это означает, что ведение журнала не будет замедлять нормальную работу.
Диагностика журнала в реальном времени
В дополнение к ведению журнала консоли я счел полезным добавить представление, показывающее состояние очередей загрузки и применения. В этом плавающем окне показано, сколько операций каждого типа ожидают отправки в iCloud и применения к устройству.
При нажатии на окно отобразятся записи в очереди, а при нажатии на запись отображаются дополнительные сведения:
Это представление скрыто для версии приложения в App Store, но оно невероятно полезно при разработке.
Журнал диагностики
Наконец, я расширил информацию, включенную в журнал диагностики, который пользователи могут отправлять мне при возникновении проблемы.
Этот журнал теперь содержит сведения о конфигурации CloudKit, а также содержимое очередей загрузки и применения.
Информация в этом отчете должна оказаться бесценной для отслеживания основной причины, если пользователи сообщают о проблемах.
Конец
И на этом сериал заканчивается. Я надеюсь, что это было полезно. Пожалуйста, присылайте любые ошибки, упущения или отзывы на @wheelies
Прежде чем я уйду, я хотел бы поделиться некоторыми статьями о CloudKit, которые я считаю невероятно полезными при добавлении поддержки CloudKit в Shopping UK:
- Во-первых, есть официальная документация Apple CloudKit. Это довольно хорошо.
- Guilherme Rambo дает прекрасный обзор CloudKit.
- У Kuba Suder есть очень полезный справочник по CloudKit и шпаргалка.
- Яанус Касе написал несколько отличных статей в блоге об использовании CloudKit для создания приложения для обмена сообщениями.
- У Дэна Гриффина есть пять отличных советов по обмену данными с CloudKit.
- У Филипа Немечека есть подробное руководство по настройке подписок.
- У Барта Джейкобса есть очень полезное руководство по устранению неполадок, когда уведомления о подписке не приходят.