На WWDC 2019, на сессии Advances in UI Data Sources, был презентован новый способ построения данных для отображений в коллекции.
В этой статье будет показан способ построения вложенных элементов в виде категорий для скрывающих и раскрывающих ячеек. Исходный код этого приложения — в конце статьи.
Если вернуться в эпоху стандартного построения таблиц и коллекций, то как правило существует два способа реализации этого поведения:
1. Увеличивать высоту ячейки при нажатии на неё и в увеличенной ячейке отображать нужный контент
Плюсы:
- Простота реализации
Минусы:
- Сложность в построении ячеек с множеством вложенностей
- Необходимость использования дополнительных флагов для определения состояния ячейки (свернут/развернут)
- Не очевидный контекст изменения высоты ячейки и поведения свернутой/развернутой ячейки
2. Добавлять и удалять ячейки при нажатии
Плюсы:
- Контекст использования данного подхода более очевиден по сравнению с предыдущим вариантом
Минусы:
- Сложность реализации. Я думаю, многие из вас сталкивались с подобными ситуациями:
- Необходимость использования дополнительных флагов для определения состояния ячейки (свернут/развернут)
- Сложность в построении ячеек с множеством вложенностей
DiffableDataSource избавляет нас от всех этих минусов. Необходима только минимальная проверка на отсутствие дополнительных вложенностей в категории. Это нужно для корректной отрисовки ячеек. Например, для категория у которых больше нету подкатегорий нам не нужно в ячейке отрисовывать accessory в виде стрелки.
Разделим реализацию на 2 части:
1. Построение моделей
2. Настройка DiffableDataSource
Построение моделей
Так как мы оперируем категориями (в нашем случае это будут: Cars, Movies, Tech) — то нам необходимо создать модель, которая будет содержать в себе название категории и её подкатегории.
Здесь стоит отметить, что класс CategoryItem должен реализовывать 2 протокола для того чтобы его можно было использовать в качестве основной модели в коллекции. Коллекции для этого нужно уметь «идентифицировать» объекты. В качестве идентификатора у нас будет выступать хеш объекта.
Последнее, что нам нужно сделать в этом пункте, так это наполнить сами данные. Для этого можно создать специальный сервис закрытый протоколом, который достает данные из сети, либо из хранилища. Я решил упростить реализацию для понимания основной сути и просто зашил эти данные в коде.
На этом этапе всё, переходим к следующей части.
Настройка DiffableDataSource
Перед тем как настроить dataSource нужно проделать небольшую подготовительную работу в виде настройке View «слоя» и UICollectionViewLayout. В основном контроллере стоит обратить внимание на 2 момента:
- Создаём enum в котором есть 1 перечисление с названием main. Оно будет выступать в качестве основной секции, в которой будут встраиваться категории.
- Объявляем два свойства: UICollectionView и UICollectionViewDiffableDataSource. Я вынес типы для UICollectionViewDiffableDataSource в typealias для простоты чтения.
Далее нам нужно создать объект типа UICollectionViewLayout. Многие на этом моменте пугаются, но здесь всё просто.
Здесь создаётся конфигурация с типом отображения sidebar. Тут можно поиграться и выбрать любой другой тип. Фон задаётся белый, по умолчанию серый. И наконец, создаём UICollectionViewCompositionalLayout в виде списка, в качестве настройки указываем наш вновь созданный конфигуратор.
Во viewDidLoad вызываем метод в котором задаём констрейнты для нашей коллекции на весь экран.
В сториборде я уже заранее обернул контроллер в NavigationController.
Теперь переходим непосредственно к настройке DiffableDataSource.
Вначале нам нужно создать регистратор ячеек. Этот блок будет вызваться с помощью внутренних механизмов коллекции для каждой ячейки и передавать туда саму ячейку, которую нужно настроить, indexPath и объект нашей категории-модели. Внутри используем defaultContentConfiguration, так как в нем уже предварительно заданы стили и текст для конфигурации назначаем из нашей модели. На что здесь стоит обратить внимание, так это на установку свойства accessories. Нам нужно проверить, есть ли у категории подкатегории. Если подкатегорий нету, то стрелку рисовать не нужно. Здесь так же можно поиграться с остальными стилями и настройками отображения.
Теперь инициализируем наше свойство dataSource. В блоке, для каждой ячейки, нам нужно задать ранее нами созданный регистратор. Здесь стоит отметить что для разных indexPath может быть применен отдельный регистратор со своим набором стилей и отображений.
Последнее что нам осталось сделать, так это применить к dataSouce снепшот с данными.
В методе categoriesSnapshot добавляем категории в снепшот. Первоначальный вызов отправляет всё дерево категорий из CategoryItemsBuilder в метод addItems, а далее, уже рекурсивным обходом дерева, добавляем все необходимые категории в снепшот. Базовым условием выхода является отсутствие подкатегорий у модели.
Единственным штрихом остаётся адаптировать UICollectionViewDelegate для нашего ViewController. Внутри мы объявляем метод
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) и снимаем выделение при нажатии на ячейку.
На этом вся наша работа завершена :)
В заключении хотелось бы добавить что данный подход избавляет нас от необходимости отслеживать нужные IndexPath в которые мы хотим добавить или удалить подкатегории, как это было отмечено в предыдущих подходах. Так же, рассмотренный нами способ, лишает нас от ненужных крашей, дебаг которых даётся не просто. Рекомендую так же дополнительно ознакомится со всеми возможностями DiffableDataSouce на WWDC сессиях.
Ссылка на готовый проект: https://github.com/david8lumen/MultiCollapsableCells
Автор: Григорян Давид