Привет, Хабр! Меня зовут Евгений, я ведущий iOS-разработчик в Туту. В нашем продукте список карточек используется неоднократно, а в проекте можно встретить несколько вариантов реализации для разных версий SDK: Через старую добрую UITableView. С использованием UICollectionView и UICollectionViewFlowLayout под iOS 11+. На связке UICollectionView и UICollectionViewCompositionalLayout для iOS 13+. Не так давно я решил резюмировать накопленный опыт реализации списка карточек и поделиться наработками в виде исходников на Github (их вы найдете в конце статьи). Одна из первых реализаций списка карточек в приложении выглядит следующим образом: Берём UITableView. Размещаем в ячейке на заднем фоне RoundedView, для которой можно установить цвет фона, параметры закругления и тени. В ТableView(_:cellForRowAt:) задаём значение СornerRaduis и направление тени для первой и последней ячейки в секции. В низ ячейки добавляем сепаратор — вьюшку, залитую цветом с фиксированной высотой, которую будем скрывать в ТableView(_:cellForRowAt:), если элемент является последним. В результате получаем составную карточку: Как это представлено в коде?   Наша первая реализация имеет ряд ограничений, в частности: ViewController является делегатом и датасорсом для таблицы, что противоречит open–closed principle. Если нам потребуется изменить класс ячейки или добавить новый вид ячеек, придётся залезать внутрь контроллера. Принцип декларирует, что программные сущности (классы, модули, функции и т.п.) должны быть открыты для расширения, но закрыты для изменения.    Каждый тип ячейки, используемый в списке, является частью карточки. На практике это часто приводит к дублированию кода между классами ячеек — принцип DRY. Don't Repeat Youself — принцип заключается в том, что нужно избегать повторений одного и того же кода.  Достаточно сложно подобрать правильные параметры тени для карточки, так как она не является цельной как у дизайнеров в Figma. Также нужно учитывать, что тень карточки находится внутри ячейки и необходимо предоставить место для её отображения, чтобы её не обрезало.   Одним из преимуществ UITableView над UICollectionView является возможность удаления элемента списка свайпом. В текущем подходе у нас возникли проблемы с закруглениями для первой и последней ячейки.   Для решения этой проблемы мы использовали вспомогательные ячейки для первой и последней позиции в секции, предоставляющие тень и закругление. Для таких ячеек отключена возможность свайпа.   Стоит отметить и преимущества данного решения: работает для всех версий SDK и просто в реализации. Предыдущее решение является простым, но недостаточно гибким и удобным. Из первого ограничения следует желание вынести DataSource в отдельную сущность, тем самым разгрузив контроллер от лишней ответственности. Яркими примерами реализации подхода являются RxDataSource и UITableViewDiffableDataSource /UICollectionViewDiffableDataSource. Второй пункт наводит нас на мысль, что классы ячеек не должны зависеть от способа представления списка. Нам необходимо вынести логику по отрисовке карточки за рамки ячеек, тем самым снизив дублирование кода. На этом этапе мы решили расстаться с UITableView в пользу UICollectionView. Решение для третьей проблемы вытекает из предыдущего пункта. Если карточка является цельной, то и настройки для неё задаются один раз. На сдачу мы можем более точно отрисовать тень карточки в соответствии с дизайном. Что касается удаления свайпом, то им пришлось пожертвовать. Возможность удаления свайпом в UICollectionView открывается для iOS 14+. В результате должно получиться следующее представление: Второй этап эволюции — карточки для списка заказов. Решение по своей концепции похоже на представленное ниже для iOS 13, с небольшими отличиями в инструментах. Вместо эплового DiffableDataSource используем RxDataSource, а UICollectionViewCompositionalLayout заменяет UICollectionViewFlowLayout в паре с декораторами, представленными UICollectionViewLayoutAttributes. Довольно часто на практике, несмотря на использование различных архитектурных паттернов, ViewController всё равно выглядит достаточно массивным. Причиной этого является нарушение принципа single responsibility . Но если всё же строго следовать принципу, то для отображения списка карточек нам потребуется 20+ строчек кода: Из сниппета следует следующее: Контроллер не занимается вёрсткой, вёрстка представлена рутовой вью. Контроллер не датасорсит коллекцию, используем выделенную сущность PassengersDataSource. Реализуем dependency inversion посредством инъекции датасорса для уменьшения связанности объектов. Выделение вёрстки в отдельный класс UIView не является обязательным и используется нечасто. Но, несомненно, такой простой подход может значительно снизить нагрузку на контроллер. Всё, что нам нужно для отображения списка, представлено в PassengersView: Однако Single Responsibility приводит к появлению дополнительных сущностей:           PassengersDataSource и PassengersLayout: PassengersLayout Как должен выглядеть список? Какого размера должны быть элементы списка? За это отвечает UICollectionViewLayout. Помимо этого c iOS 13+ UICollectionViewLayout позволяет добавлять декораторы как для элемента группы, так и для всей секции. В нашем случае декораторами являются: карточка, сепаратор и заголовок секции.   Для создания PassengersLayout используется фабричный метод, позволяющий управлять созданием лайаута вдали от нашего вью контроллера:  Для отображения списка карточек нам необходимо:  При создании элемента списка в _makeItem() добавить сепаратор вниз ячейки: При создании секции в _makeSection(for: group) подложить карточку на задний фон и добавить заголовок:    При создании лайаута в _makeLayout(for: section) зарегистрировать класс, представляющий карточку:   Используемые декораторы можно разделить на два типа:   SupplementaryView (заголовки и сепараторы). DecorationView (карточка).  Коллекция позволяет максимально гибко работать с SupplementaryView. Всё, что мы сообщили про наши SupplementaryView-коллекции, это позицию вью, размер и идентификатор. Конкретную реализацию будет предоставлять наш PassengersDataSource.  В iOS 13 на помощь к MassiveViewController приходит UITableViewDiffableDataSource и UICollectionViewDiffableDataSource. Помимо преследования SRP, Apple значительно упростил обновление элементов списка.   Для создания DataSource нам потребуется коллекция и провайдер ячеек init(collectionView :cellProvider:).  PassengersDataSource является алиасом для UICollectionViewDiffableDataSource с заданным типом модели для заголовка PassengerSectionHeaderView .ViewState и для ячейки PassengerCell.ViewState.   Осталось сообщить датасорсу, как создавать ячейки, карточку и сепаратор.   Продолжая разделять ответственность, получаем ещё две cущности:   PassengerCellProvider и PassengersViewProvider: Структуру провайдера ячеек нам задаёт сам DataSource в виде замыкания:  Чаще всего регистрация ячеек находится во вью контроллере, что в нашем случае приведёт к нарушению принципа подстановки Барбары Лисков. Если мы добавим новый тип ячеек, то получим знакомый краш в рантайме. Поэтому регистрацию ячеек необходимо проводить непосредственно перед созданием провайдера:    Для создания заголовков секции и сепаратора используется тот же подход, что при создании ячейки. Нам необходим провайдер, который будет регистрировать и предоставлять метод конфигурации SupplementaryView.  Вспомним, что наш DataSource принимает один вариант SupplementaryViewProvider, но у нас их несколько. Для этого воспользуемся паттерном Composite и создадим композитный провайдер:    Если для отрисовки списка потребуется больше одной ячейки, аналогичным образом  заведём CompositePassengerCellsProvider. Почему не сделать сразу? Потому что Keep it simple, stupid.  В результате наше решение выглядит следующим образом:   Исходники на github.com.   Senior iOS Developer at Bolt