После просмотра Keynote WWDC 2019 и знакомства с SwiftUI, предназначенного для декларативного описания UI в коде, хочется порассуждать о том, как можно декларативно наполнять таблички и коллекции. Например, вот так: В коллекции это отрисуется следующим образом:  Как известно из авторитетных источников: подавляющее большинство своего времени типичный iOS-разработчик проводит за работой с табличками. Если предположить, что разработчиков на проекте катастрофически нехватает и таблички не из простых, то времени на остальные части приложения не остается совсем. И с этим надо что-то делать… Возможным решением будет декомпозиция ячеек. Под декомпозицией ячеек подразумевается замена одной ячейки несколькими ячейками меньшего размера. При такой замене визуально ничего не должно измениться. В качестве примера можно рассмотреть посты из новостной ленты VK для iOS. Один пост можно представить как в виде одной ячейки, так и в виде группы ячеек — примитивов. Декомпозировать ячейки получится не всегда. Сложно будет разбить на примитивы ячейку, которая имеет тень или скругление со всех сторон. В таком случае исходная ячейка и будет являться примитивом. Используя декомпозицию ячеек, таблицы/коллекции начинают состоять из примитивов, которые часто будут переиспользоваться: примитив с текстом, примитив с картинкой, примитив с фоном и т.п. Вычисление высоты отдельно взятого примитива гораздо проще и эффективнее, чем сложной ячейки с большим количеством состояний. При желании динамическую высоту примитива можно посчитать или даже отрисовать в бэкграунде (например, текст через CTFramesetter). С другой стороны усложняется работа с данными. Данные будут требоваться для каждого примитива и по IndexPath примитива сложно будет определить к какой реальной ячейке он относится. Придется вводить новые слои абстракции или еще как-то решать эти проблемы. Можно долго рассуждать о возможных плюсах и минусах данной затеи, но лучше попробовать описать подход к декомпозиции ячеек. Так как UITableView ограничена в своих возможностях, а у нас, как уже упоминалось, достаточно сложные таблички, то адекватным решением будет использование UICollectionView. Именно о UICollectionView и пойдет речь в данной публикации. Используя UICollectionView сталкиваешься с ситуацией, когда базовый UICollectionViewFlowLayout не может сформировать требуемое расположение элементов коллекции (новый UICollectionViewCompositionalLayout в расчет не берем). В такие моменты обычно принимается решение найти какой-нибудь open-source UICollectionViewLayout. Но и среди готовых решений может не быть подходящего, как, например, в случае с динамической главной страницей крупного интернет-магазина или социальной сети. Предполагаем худшее, поэтому будем создавать собственный универсальный UICollectionViewLayout. Помимо сложностей с выбором лейаута, требуется принять решение о том, как коллекция будет получать данные. Кроме обычного подхода, где объект (чаще всего UIViewController) соответствует протоколу UICollectionViewDataSource и предоставляет данные для коллекции, набирает популярность использование data-driven фреймворков. Яркими представителями такого подхода являются CollectionKit, IGListKit, RxDataSources и другие. Использование подобных фреймворков упрощает работу с коллекциями и предоставляет возможность анимировать изменения данных, т.к. diffing алгоритм уже присутствует в фреймворке. Для целей публикации будет выбран фреймворк RxDataSources. Введем промежуточную структуру данных и назовем ее виджетом. Опишем основные свойства, которыми должен обладать виджет: Чтобы была возможность использовать виджет с фреймворком RxDataSources, он должен соответствовать протоколам Equatable и IdentifiableType. Так как виджет представляет примитив, то для целей публикации будет достаточным, если виджет будет сам себя идентифицировать для соответствия протоколу IdentifiableType. На практике это скажется на том, что при изменении виджета будет происходить не перезагрузка примитива, а удаление и вставка. Для этого введем новый протокол WidgetIdentifiable: Чтобы соответствовать WidgetIdentifiable, виджету требуется соответствовать протоколу Hashable. Данные для соответствия протоколу Hashable виджет будет брать из объекта, который будет описывать конкретный примитив. Можно использовать AnyHashable для "стирания" виджетом типа объекта. На этом этапе первые два свойства виджета выполняются. Это нетрудно проверить собрав в массив несколько виджетов с разными типами объектов. Для реализации оставшихся свойств введем новый протокол WidgetPresentable Функция widgetSize(containerWidth:) будет использоваться в UICollectionViewLayout при формировании атрибутов ячеек, а widgetCell(collectionView:indexPath:) — для получения ячеек. При соответствии виджета протоколу WidgetPresentable, виджет будет выполнять все обозначеные в начале публикации свойства. Однако, содержащийся внутри виджета AnyHashable объект придется заменить на композицию WidgetPresentable и WidgetHashable, где WidgetHashable не будет иметь ассоциированного значения (как в случае с Hashable) и тип объекта внутри виджета останется "стертым": В конечном варианте виджет будет выглядеть следующим образом: Попробуем собрать простейший примитив, который будет представлять из себя отступ заданной высоты. WidgetCell<T> — это всего лишь сабкласс UICollectionViewCell, который принимает UIView и добавяет ее как сабвью. cellDequeueSafely(indexPath:) — это функция, которая регистрирует ячейку в коллекции перед переиспользованием, если ранее ячейка в коллекции зарегистрирована не была. Использоваться Spacing будет так, как это описано в самом начале публикации. После получения массива виджетов, его останется лишь забиндить на observerWidgets: В заключении хотелось бы показать реальную работу коллекции, которая построена целиком на виджетах. Как можно увидеть, декомпозиция UICollectionViewCell осуществима и в подходящих ситуациях способна упростить жизнь разработчику. Приведенный в публикации код является очень упрощенным и не должен собираться. Целью было описать подход, а не предоставить готовое решение. Протокол WidgetPresentable можно расширить и другими функциями, позволяющими оптимизировать лейаут, например, widgetSizeEstimated(containerWidth:) или widgetSizePredefined(containerWidth:), которые возвращают предполагаемый и фиксированный размер соответственно. Стоит отметить, что функция widgetSize(containerWidth:) должна возвращать размер примитива даже для ресурсоемких вычислений, например, для systemLayoutSizeFitting(_:). Подобные вычисления можно кэшировать через Dictionary, NSCache и т.п. Как известно, все типы ячеек, используемые UICollectionView, должны быть предварительно зарегистрированы в коллекции. Однако, чтобы переиспользовать виджеты между разными экранами/коллекциями и не регистрировать заранее все идентификаторы/типы ячеек, необходимо обзавестись механизмом, который будет регистрировать ячейку непосредственно перед ее первым использованием в рамках каждой коллекции. В публикации для этого использовалась функция cellDequeueSafely(indexPath:). В коллекции может не быть ни хедеров, ни футеров. На их месте будут примитивы. Наличие supplementary в коллекции никаких особых бонусов не даст в текущем подходе. Обычно их используют когда массив данных строго соответствует количеству ячеек и требуется показать дополнительные вьюшки перед, после или между ячейками. В нашем случае данные для вспомогательных вьюх также можно добавлять в массив виджетов и рисовать как примитивы. В рамках одной коллекции могут находиться виджеты с одинаковыми объектами. Например, одинаковый Spacing в начале и в конце коллекции. Наличие таких неуникальных объектов приведет к тому, что анимация в коллекции пропадет. Чтобы сделать подобные объекты уникальными, можно использовать специальные AnyHashable тэги, #file и #line места создания объекта и т.п.