[Из песочницы] «Используйте стандартный контрол» или как мы воровали календарь у Apple

ab09cf3429334b2ebef29372d84857af.png В данной статье хотелось бы познакомить читателей с довольно распространенной задачей по созданию календаря, которая была поставлена нашей команде в рамках одного проекта.Желание поделится опытом по преодолению трудностей, с которыми приходится сталкиваться при реализации такого рода приложений, возникло в основном потому, что мы не обнаружили готового решения, устраивающего нас по производительности.

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

Всех заинтересованных прошу под кат.

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

Представление года в стандартном календаре можно увидеть ниже.

95eebb05b548466497f063dea2c1943f.png

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

e87871b67d0148f788637bdec679544e.png

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

Далее в статье будет описан процесс создания именно годового представления как наиболее требовательного к ресурсам системы.

Все работы по оптимизации работы календаря начались с поиска быстрого способа отрисовки ячейки таблицы. Мы решили использовать таблицу или UITableView как для представления месяца, так и для представления года. Для представления года, данное решение было выбрано, чтобы данные было удобно подтягивать сразу для целого года, а не для отдельных месяцев, как это было бы в случае использования UICollectionView.

Вначале мы решили попробовать создать интерфейс, используя простые и популярные средства, такие как компоненты библиотеки UIKit. Было решено сделать массив из UILabel, удовлетворяющий всем позициям дат в календаре, которых для одного месяца насчитывается 7×6 = 42.

4fb8a5c7b6c84a0094f537746a04ed2d.png

Затем, как это уже очевидно, текст с числом дня подставлялся в нужную UILabel без изменения позиции самой лейблы. В итоге получилось, что календарь жутко тормозил и залипал. Стало интересно почему, и при помощи Time Profiler удалось установить причину. Беда заключалась в функции setText, а если более подробно, то в механизме конвертации массива char-ов в глифы для определенной даты и дальнейшей отрисовке.

Поэтому следующим шагом было создании массива UILabel в количестве 31-ой штуки для максимального количества дней в месяце с заранее заданным текстом-числом. При этом способе мы манипулируем координатами определенной лейблы.

464a717e10274fe8be9742a615fe9211.png

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

Дальнейшие наши исследования были устремлены в сторону библиотеки Quartz. Использовали ту же систему с заранее заданными числами для 31-ой даты, что и в способе с лейблами. Также в зависимости от месяца устанавливали координаты слоев, не изменяя строкового содержания. В итоге средний FPS удалось увеличить до 44.

Последним вариантом было использование CoreText. С помощью него удалось добиться требуемой скорости работы годового представления календаря и 53 FPS, однако далее нас ожидал очередной челенж.

Стандартный календарь iOS 7.0 работает очень плавно, но у него нет функции отображения событий для конкретного дня, которые можно увидеть для представления месяца в виде точки под датой.

7006c75c01634f0f9c8d75cd33ac0d4c.png

Мы хотели, чтобы такая функция была доступна и для гoдового календаря. Вот тут снова начали возникать проблемы с производительностью, так как при установке события, которое повторяется ежедневно, FPS начинал проседать. Проблема уже была связана не с отрисовкой текста, а с рисованием бэкграунда позади даты. Поэтому решили рисовать все события в отдельном потоке, после чего конвертировать графический контекст в объект UIImage. UIImage является thread-safe и поэтому отрисовка событий выглядела бы как подстановка новой картинки для определенного месяца.

Для работы с многопоточностью в iOS доступно множество решений. В нашем случае оптимальным решением было использование классов наследников NSOperation и NSOperationQueue. В ином случае при использовании, к примеру, GCD (Grand Central Dispatch) возникла бы проблема с отменой подгрузки данных в календарь, из-за чего пришлось бы писать дополнительную обертку.

На данном этапе решили сразу рассмотреть и проблему с подгрузкой данных из БД. В проекте использовалась CoreData со всеми вытекающими проблемами по асинхронному извлечению, так как экземпляры NSManagedObject и NSManagedObjectContext не являются thread-safe. Чтобы преодолеть данную особенность фреймворка, приватный NSManagedObjectContext создается при выполнении функции main экземпляра NSOperation, которая выполняется в отдельном потоке. Это позволило нам объединить два действия в отдельном потоке, а именно:

извлечение событий для календаря из базы данных, рисование и создание картинки в отдельном потоке. Работу календаря по отображению одного года также можно проиллюстрировать следующей диаграммой: 237162d8480846cf965a4b68b159ed04.png

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

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

Экраны приложения с календарем, как это в итоге получилось у нас, можно посмотреть ниже.

5fa1956942c54c2db96e4d8311fbf7d2.png

P.S. Исходный код проекта-примера тут.

P.P. S. Также очень рассчитываю на обратную связь по другим возможным вариантам решения похожей задачи.

© Habrahabr.ru