[Разработка под iOS, Разработка мобильных приложений, Интерфейсы] Compositional Layout: стоит ли игра свеч?
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всем привет! Меня зовут Алексей Скоробогатов, я ведущий iOS-разработчик в Delivery Club. Сегодня я хотел бы рассказать про вёрстку в нашем приложении на примере использования Compositional Layout. В конце прошлого года волевым решением iOS-команды и апрувом руководства мы перешли на iOS 13+. Этот манёвр позволил нам начать использовать новые нативные инструменты, в том числе и новый декларативный подход к описанию layout-коллекций. Расскажу о переводе нашего экрана поиска и его компонентов на Compositional Layout, а также о проблемах, с которыми я столкнулся.
Для полноценного представления о Compositional Layout предлагаю прочитать вот эту статью и ознакомиться с примерами Apple.
Экскурс в экран и его компоновку
Давайте посмотрим, как экран выглядел на iOS 12. Классический подход к вёрстке, вертикальная коллекция с ячейками, которые могут содержать горизонтальные коллекции со своими ячейками.
В чём основные минусы такого подхода?
- Для каждой горизонтальной коллекции нужно делать свою ячейку с UICollectionView, со всем бойлерплейтом (настройкой делегатов и прочим).
- Иногда встречаются проблемы с анимациями и auto-sizing ячейками.
- Поддержка экрана превращается в настоящее приключение.
Так что же нам даёт Compositional Layout?
- Мы можем описать весь layout экрана в декларативном стиле в одном месте.
- Перестать использовать связку UICollectionviewCell с UICollectionView, и использовать только конкретные ячейки.
- Легко описывать auto-sizing ячейки.
Внедрение Composition Layout
Давайте перейдём к практике. Для начала заменим стандартный flowLayout на UICollectionViewCompositionalLayout:
private lazy var collectionView: UICollectionView = {
let collectionViewLayout = UICollectionViewCompositionalLayout { [weak self] (sectionIndex, environment) -> NSCollectionLayoutSection? in
guard let self = self else { return nil }
let section = self.viewModel.sections[sectionIndex]
switch section.kind {
//Тут мы должны вернуть NSCollectionLayoutSection для конкретной секции
}
}
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.dataSource = self
collectionView.delegate = self
//Регистрируем ячейки компонентов
return collectionView
}()
Как видите, существенно ничего не изменилось, у нас просто появился handler для описания каждой конкретной секции. Рассмотрим основные классы Compositional Layout, которые мы будем использовать:
- NSCollectionLayoutItem — отвечает за layout конкретного элемента;
- NSCollectionLayoutGroup — отвечает за layout группы элементов;
- NSCollectionLayoutSection — описывает layout конкретной секции.
Методы UICollectionViewDelegateFlowLayout удаляем, они нам не понадобятся. Конфигурирование ячеек остаётся без изменений и происходит через cellForItemAtIndexPath:
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let section = viewModel.sections[indexPath.section]
let item = section.items[indexPath.row]
switch item {
case .restaurant:
//Настройка UICollectionViewCell для элемента карточки ресторанов
return UICollectionViewCell()
}
}
Коллекция карточек
Рассмотрим подробно самый распространенный компонент на экране — коллекцию горизонтальных карточек.
Опишем реализацию секции:
func restaurantsSectionLayout(sectionItems: [SearchViewModel.Item],
environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection {
let contentSize = environment.container.contentSize
let itemSize = NSCollectionLayoutSize(
widthDimension: .absolute(contentSize.width * 0.6),
heightDimension: .estimated(216)
)
let items: [NSCollectionLayoutItem] = sectionItems.map({ _ in
.init(layoutSize: itemSize)
})
//Тут можно рассчитать приблизительный размер горизонтальной группы
let groupEstimateSize = CGSize(width: 50, height: 50)
let groupSize = NSCollectionLayoutSize(
widthDimension: .estimated(groupEstimateSize.width),
heightDimension: .estimated(groupEstimateSize.height)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: items)
group.interItemSpacing = .fixed(12)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
section.boundarySupplementaryItems = [headerItem]
section.supplementariesFollowContentInsets = false
return section
}
На вход функции мы подаём массив элементов в коллекции, чтобы полноценно описать наш layout компонента и NSCollectionLayoutEnvironment, отвечающий за информацию о размере контейнера.
Для начала опишем размер наших элементов в коллекции с помощью NSCollectionLayoutSize. В этом конкретном примере ширина будет иметь абсолютное значение, рассчитанное с учётом размеров контейнера, а высота будет автоматически рассчитана на этапе рендеринга.
let contentSize = environment.container.contentSize
let itemSize = NSCollectionLayoutSize(
widthDimension: .absolute(contentSize.width * 0.6),
heightDimension: .estimated(216)
)
Далее нам надо описать группу, которая будет содержать все элементы. Укажем, что размер нашей группы groupSize должен быть рассчитан на этапе рендеринга, также зададим расстояние между элементами коллекции через interItemSpacing.
//Тут можно рассчитать приблизительный размер горизонтальной группы
let groupEstimateSize = CGSize(width: 50, height: 50)
let groupSize = NSCollectionLayoutSize(
widthDimension: .estimated(groupEstimateSize.width),
heightDimension: .estimated(groupEstimateSize.height)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: items)
group.interItemSpacing = .fixed(12)
Самое главное — не забыть указать, что наша коллекция будет горизонтальной, для этого выставим:
section.orthogonalScrollingBehavior = .continuous
Как видите, у компонента также присутствует блок с заголовком и стрелкой перехода на полный список. Вы, наверное, уже догадались, что это Supplementary View, его тоже можно легко описать через класс NSCollectionLayoutBoundarySupplementaryItem, где укажем размер и расположение:
private var headerItem: NSCollectionLayoutBoundarySupplementaryItem {
let headerSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(50)
)
return NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .topLeading
)
}
Далее добавляем к секции наш Supplementary View. Чтобы заголовок игнорировал contentInsets нашей секции, выставим supplementariesFollowContentInsets в false:
section.boundarySupplementaryItems = [headerItem]
section.supplementariesFollowContentInsets = false
Вот и всё, что нам потребовалось для описания секции.
Коллекция карточек кухонь
Особенность этой секции заключается в вертикальном расположении карточек по два элемента на каждую строку:
Настройку NSCollectionLayoutSection для этой секции делаем аналогично предыдущей:
func kitchenSectionLayout() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(128)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitem: item,
count: 2
)
group.interItemSpacing = .fixed(12)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 12
return section
}
Размер элемента указываем зависимым от размеров контейнера. Размер группы задаём зависимым от контейнера по ширине и с абсолютным значением высоты. Саму группу мы описываем как горизонтальный контейнер, состоящий из двух элементов, с заранее заданным расстоянием между элементами. Так как у нас будет множество таких групп, расположенных вертикально, не забываем указать расстояние между этими группами: interGroupSpacing. Кнопку «Показать все» задаём как Supplementary View аналогично headerItem, за исключением того, что это elementKindSectionFooter, и располагается он под секцией.
private var footerButtonItem: NSCollectionLayoutBoundarySupplementaryItem {
let footerSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(50)
)
return NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: footerSize,
elementKind: UICollectionView.elementKindSectionFooter,
alignment: .bottom
)
}
Коллекция блюд
Компонент очень похож на коллекцию карточек, настройка секции внутри схожа с коллекцией карточек, за исключением размеров item, тут они заданы в абсолютных величинах.
Но не всё так просто, куда же без проблем. По задумке дизайнеров карточки должны были скролиться в рамках оранжевого фона, но по факту ограничить прокрутку через NSCollectionLayoutSection не получалось, и элементы прокручивались через весь экран.
Поэтому намучившись несколько часов с документацией и гуглом, пришлось вернуться к истокам и по старинке использовать связку UICollectionViewCell и UICollectionView.
Проблемы
На данный момент это все компоненты на этом экране, теперь я бы хотел поговорить о проблемах и костылях, с которыми мне пришлось столкнуться при внедрении Compositional Layout.
Не очень гибкий self-sizing для ячеек
С помощью NSCollectionLayoutEdgeSpacing для секции довольно просто описываются варианты, когда ячейки должны быть прижаты к краям контента (top, bottom, left, right), но вот указать, чтобы ячейки растягивались до максимального размера элемента в группе, мне так и не удалось, поэтому такие дизайнерские решения сейчас отклоняются.
Работа с горизонтальными коллекциями
В коллекции карточек у нас есть динамически анимированный компонент SupplementaryItem, прижатый к правому краю, который ведёт на экран со списком. Его анимация зависит от contentOffset и contentSize горизонтального UIScrollView.
Если раньше для получения этих данных мы использовали метод scrollViewDidScroll у делегата UICollectionView в UICollectionViewCell, то теперь возник вопрос, как быть с Compositional Layout. Первое, что пришло в голову, — подписаться на делегат основного вертикального UICollectionView, но я сразу получил оплеуху в виде того, что в scrollViewDidScroll падают события только вертикальной прокрутки. Резонный вопрос: почему? Оказывается, инженеры Apple для горзинтальных коллекций используют приватный класс _UICollectionViewOrthogonalScrollerEmbeddedScrollView, который является наследником UIScrollView, у которого subviews — это UICollectionVIewCell.
Как обычно, легального способа добраться до этого scrollView Apple нам не оставила, но дала доступ к visibleItemsInvalidationHandler в NSCollectionLayoutSection, который будет вызываться при прокрутке:
// Called for each layout pass to allow modification of item properties right before they are displayed.
open var visibleItemsInvalidationHandler: NSCollectionLayoutSectionVisibleItemsInvalidationHandler?
Как видно, на входе мы будем иметь массив visibleItems, contentOffset и environment.
public typealias NSCollectionLayoutSectionVisibleItemsInvalidationHandler = ([NSCollectionLayoutVisibleItem], CGPoint, NSCollectionLayoutEnvironment) -> Void
В итоге для простых вещей этого должно хватить, но, увы, если вам нужен доступ к другим методам UIScrollViewDelegate, то без костылей вам не обойтись, так как UICollectionViewOrthogonalScrollerEmbeddedScrollView — приватный и уже имеет свой системный делегат.
Баг с горизонтальными коллекциями
После релиза в продакшен нового layout, у нас в поддержку стали падать тикеты о том, что иногда горизонтальные коллекции не реагируют на нажатия и скроллинг. Исследование этой проблемы завело меня на старый добрый StackOverflow, и как оказалось, в версии iOS 14.3+ инженеры Apple сломали расположение горизонтального UIScrollView, когда вы используете estimate в NSCollectionLayoutSize.
Этот баг у нас проявился в корзине с рекомендациями: как видите, коллекция визуально находится на своём месте, но вот её UICollectionViewOrthogonalScrollerEmbeddedScrollView (подсвечен синим) распологается в неправильном месте.
Workaround этого бага оказался немного костыльно-ориентированным и заключался в том, чтобы пробежаться по subviews у UICollectionView и починить руками сломанные фреймы. Оценить решение можно здесь.
Выводы
Так стоит ли использовать новый layout или нет? Как показала практика, технология пока не идеальна и имеет много нюансов и оговорок: на сложных интерфейсах можно столкнуться со множеством проблем, решение которых может заставить вас попотеть. Но, с другой стороны, можно вспомнить релиз UICollectionView в iOS 6 и сколько там было головной боли и проблем, но сейчас уже сложно представить жизнь без коллекций.
Если говорить про наш проект, то переход к декларативному описанию лейаута, позволил нам сократить время разработки компонентов. Упростилась поддержка экрана, так как почти вся информация о вёрстке находится в одном месте и имеет общий вид (раньше приходилось скакать по xib-файлам и их классам, чтобы поменять какой-нибудь отступ и т.д.). Также теперь не должно возникать проблем со сложной версткой элементов (пламенный привет всем тем, кто занимался переопределением метода layoutAttributesForElements в UICollectionViewFlowLayout).
Поэтому если у вас есть возможность и время, я настоятельно рекомендую начать использовать Compositional Layout в своих коллекциях и не бояться сопутствующих проблем.
===========
Источник:
habr.com
===========
Похожие новости:
- [Big Data, Хранилища данных, Data Engineering] Мультитул для управления Хранилищем Данных — кейс Wheely + dbt
- [Машинное обучение, Искусственный интеллект] Исследование МТИ нашло «систематические» ошибки в датасетах для обучения нейросетей
- [Облачные вычисления, Разработка под e-commerce, Управление e-commerce, Облачные сервисы] Вебинар «Технологии, которые позволяют E-commerce опередить конкурентов» 6 апреля
- [Я пиарюсь, Разработка мобильных приложений, Flutter] Storybook + Flutter = storybook_flutter
- [] ИТ-амбассадор — на шаг ближе к профессии
- [Интерфейсы, Разработка под Windows, Дизайн, Софт] В Windows 10 21H2 появится новый параметр в меню питания, изоляция сторонних драйверов и процесс taskbar.dll (перевод)
- [Информационная безопасность, Разработка под iOS, Гаджеты, Смартфоны] Apple закрыла активно используемую уязвимость нулевого дня в iPhone, iPad и Watch
- [Веб-дизайн, Интерфейсы, Читальный зал] Они рисуют дыры в бюджете. Что не так с вашими дизайнерами?
- [Разработка под iOS, Xcode, Swift] 7 Кругов SPM или как сделать модульное приложение на Swift Package Manager
- [Разработка под iOS, Разработка мобильных приложений, Swift, Дизайн мобильных приложений] iOS. UI. Приëмы. Часть 1
Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_interfejsy (Интерфейсы), #_dctech, #_delivery_club, #_compositional_layout, #_layout, #_ui, #_ios, #_uicollectionview, #_uicollectionviewlayout, #_uicollectionviewcompositionallayout, #_blog_kompanii_mail.ru_group (
Блог компании Mail.ru Group
), #_blog_kompanii_delivery_club_tech (
Блог компании Delivery Club Tech
), #_razrabotka_pod_ios (
Разработка под iOS
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_interfejsy (
Интерфейсы
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:20
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всем привет! Меня зовут Алексей Скоробогатов, я ведущий iOS-разработчик в Delivery Club. Сегодня я хотел бы рассказать про вёрстку в нашем приложении на примере использования Compositional Layout. В конце прошлого года волевым решением iOS-команды и апрувом руководства мы перешли на iOS 13+. Этот манёвр позволил нам начать использовать новые нативные инструменты, в том числе и новый декларативный подход к описанию layout-коллекций. Расскажу о переводе нашего экрана поиска и его компонентов на Compositional Layout, а также о проблемах, с которыми я столкнулся. Для полноценного представления о Compositional Layout предлагаю прочитать вот эту статью и ознакомиться с примерами Apple. Экскурс в экран и его компоновку Давайте посмотрим, как экран выглядел на iOS 12. Классический подход к вёрстке, вертикальная коллекция с ячейками, которые могут содержать горизонтальные коллекции со своими ячейками. В чём основные минусы такого подхода?
Так что же нам даёт Compositional Layout?
Внедрение Composition Layout Давайте перейдём к практике. Для начала заменим стандартный flowLayout на UICollectionViewCompositionalLayout: private lazy var collectionView: UICollectionView = {
let collectionViewLayout = UICollectionViewCompositionalLayout { [weak self] (sectionIndex, environment) -> NSCollectionLayoutSection? in guard let self = self else { return nil } let section = self.viewModel.sections[sectionIndex] switch section.kind { //Тут мы должны вернуть NSCollectionLayoutSection для конкретной секции } } let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) collectionView.dataSource = self collectionView.delegate = self //Регистрируем ячейки компонентов return collectionView }() Как видите, существенно ничего не изменилось, у нас просто появился handler для описания каждой конкретной секции. Рассмотрим основные классы Compositional Layout, которые мы будем использовать:
Методы UICollectionViewDelegateFlowLayout удаляем, они нам не понадобятся. Конфигурирование ячеек остаётся без изменений и происходит через cellForItemAtIndexPath: func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let section = viewModel.sections[indexPath.section] let item = section.items[indexPath.row] switch item { case .restaurant: //Настройка UICollectionViewCell для элемента карточки ресторанов return UICollectionViewCell() } } Коллекция карточек Рассмотрим подробно самый распространенный компонент на экране — коллекцию горизонтальных карточек. Опишем реализацию секции: func restaurantsSectionLayout(sectionItems: [SearchViewModel.Item],
environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { let contentSize = environment.container.contentSize let itemSize = NSCollectionLayoutSize( widthDimension: .absolute(contentSize.width * 0.6), heightDimension: .estimated(216) ) let items: [NSCollectionLayoutItem] = sectionItems.map({ _ in .init(layoutSize: itemSize) }) //Тут можно рассчитать приблизительный размер горизонтальной группы let groupEstimateSize = CGSize(width: 50, height: 50) let groupSize = NSCollectionLayoutSize( widthDimension: .estimated(groupEstimateSize.width), heightDimension: .estimated(groupEstimateSize.height) ) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: items) group.interItemSpacing = .fixed(12) let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .continuous section.boundarySupplementaryItems = [headerItem] section.supplementariesFollowContentInsets = false return section } На вход функции мы подаём массив элементов в коллекции, чтобы полноценно описать наш layout компонента и NSCollectionLayoutEnvironment, отвечающий за информацию о размере контейнера. Для начала опишем размер наших элементов в коллекции с помощью NSCollectionLayoutSize. В этом конкретном примере ширина будет иметь абсолютное значение, рассчитанное с учётом размеров контейнера, а высота будет автоматически рассчитана на этапе рендеринга. let contentSize = environment.container.contentSize
let itemSize = NSCollectionLayoutSize( widthDimension: .absolute(contentSize.width * 0.6), heightDimension: .estimated(216) ) Далее нам надо описать группу, которая будет содержать все элементы. Укажем, что размер нашей группы groupSize должен быть рассчитан на этапе рендеринга, также зададим расстояние между элементами коллекции через interItemSpacing. //Тут можно рассчитать приблизительный размер горизонтальной группы
let groupEstimateSize = CGSize(width: 50, height: 50) let groupSize = NSCollectionLayoutSize( widthDimension: .estimated(groupEstimateSize.width), heightDimension: .estimated(groupEstimateSize.height) ) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: items) group.interItemSpacing = .fixed(12) Самое главное — не забыть указать, что наша коллекция будет горизонтальной, для этого выставим: section.orthogonalScrollingBehavior = .continuous
Как видите, у компонента также присутствует блок с заголовком и стрелкой перехода на полный список. Вы, наверное, уже догадались, что это Supplementary View, его тоже можно легко описать через класс NSCollectionLayoutBoundarySupplementaryItem, где укажем размер и расположение: private var headerItem: NSCollectionLayoutBoundarySupplementaryItem {
let headerSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50) ) return NSCollectionLayoutBoundarySupplementaryItem( layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading ) } Далее добавляем к секции наш Supplementary View. Чтобы заголовок игнорировал contentInsets нашей секции, выставим supplementariesFollowContentInsets в false: section.boundarySupplementaryItems = [headerItem]
section.supplementariesFollowContentInsets = false Вот и всё, что нам потребовалось для описания секции. Коллекция карточек кухонь Особенность этой секции заключается в вертикальном расположении карточек по два элемента на каждую строку: Настройку NSCollectionLayoutSection для этой секции делаем аналогично предыдущей: func kitchenSectionLayout() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0) ) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(128) ) let group = NSCollectionLayoutGroup.horizontal( layoutSize: groupSize, subitem: item, count: 2 ) group.interItemSpacing = .fixed(12) let section = NSCollectionLayoutSection(group: group) section.interGroupSpacing = 12 return section } Размер элемента указываем зависимым от размеров контейнера. Размер группы задаём зависимым от контейнера по ширине и с абсолютным значением высоты. Саму группу мы описываем как горизонтальный контейнер, состоящий из двух элементов, с заранее заданным расстоянием между элементами. Так как у нас будет множество таких групп, расположенных вертикально, не забываем указать расстояние между этими группами: interGroupSpacing. Кнопку «Показать все» задаём как Supplementary View аналогично headerItem, за исключением того, что это elementKindSectionFooter, и располагается он под секцией. private var footerButtonItem: NSCollectionLayoutBoundarySupplementaryItem {
let footerSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50) ) return NSCollectionLayoutBoundarySupplementaryItem( layoutSize: footerSize, elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottom ) } Коллекция блюд Компонент очень похож на коллекцию карточек, настройка секции внутри схожа с коллекцией карточек, за исключением размеров item, тут они заданы в абсолютных величинах. Но не всё так просто, куда же без проблем. По задумке дизайнеров карточки должны были скролиться в рамках оранжевого фона, но по факту ограничить прокрутку через NSCollectionLayoutSection не получалось, и элементы прокручивались через весь экран. Поэтому намучившись несколько часов с документацией и гуглом, пришлось вернуться к истокам и по старинке использовать связку UICollectionViewCell и UICollectionView. Проблемы На данный момент это все компоненты на этом экране, теперь я бы хотел поговорить о проблемах и костылях, с которыми мне пришлось столкнуться при внедрении Compositional Layout. Не очень гибкий self-sizing для ячеек С помощью NSCollectionLayoutEdgeSpacing для секции довольно просто описываются варианты, когда ячейки должны быть прижаты к краям контента (top, bottom, left, right), но вот указать, чтобы ячейки растягивались до максимального размера элемента в группе, мне так и не удалось, поэтому такие дизайнерские решения сейчас отклоняются. Работа с горизонтальными коллекциями В коллекции карточек у нас есть динамически анимированный компонент SupplementaryItem, прижатый к правому краю, который ведёт на экран со списком. Его анимация зависит от contentOffset и contentSize горизонтального UIScrollView. Если раньше для получения этих данных мы использовали метод scrollViewDidScroll у делегата UICollectionView в UICollectionViewCell, то теперь возник вопрос, как быть с Compositional Layout. Первое, что пришло в голову, — подписаться на делегат основного вертикального UICollectionView, но я сразу получил оплеуху в виде того, что в scrollViewDidScroll падают события только вертикальной прокрутки. Резонный вопрос: почему? Оказывается, инженеры Apple для горзинтальных коллекций используют приватный класс _UICollectionViewOrthogonalScrollerEmbeddedScrollView, который является наследником UIScrollView, у которого subviews — это UICollectionVIewCell. Как обычно, легального способа добраться до этого scrollView Apple нам не оставила, но дала доступ к visibleItemsInvalidationHandler в NSCollectionLayoutSection, который будет вызываться при прокрутке: // Called for each layout pass to allow modification of item properties right before they are displayed.
open var visibleItemsInvalidationHandler: NSCollectionLayoutSectionVisibleItemsInvalidationHandler? Как видно, на входе мы будем иметь массив visibleItems, contentOffset и environment. public typealias NSCollectionLayoutSectionVisibleItemsInvalidationHandler = ([NSCollectionLayoutVisibleItem], CGPoint, NSCollectionLayoutEnvironment) -> Void
В итоге для простых вещей этого должно хватить, но, увы, если вам нужен доступ к другим методам UIScrollViewDelegate, то без костылей вам не обойтись, так как UICollectionViewOrthogonalScrollerEmbeddedScrollView — приватный и уже имеет свой системный делегат. Баг с горизонтальными коллекциями После релиза в продакшен нового layout, у нас в поддержку стали падать тикеты о том, что иногда горизонтальные коллекции не реагируют на нажатия и скроллинг. Исследование этой проблемы завело меня на старый добрый StackOverflow, и как оказалось, в версии iOS 14.3+ инженеры Apple сломали расположение горизонтального UIScrollView, когда вы используете estimate в NSCollectionLayoutSize. Этот баг у нас проявился в корзине с рекомендациями: как видите, коллекция визуально находится на своём месте, но вот её UICollectionViewOrthogonalScrollerEmbeddedScrollView (подсвечен синим) распологается в неправильном месте. Workaround этого бага оказался немного костыльно-ориентированным и заключался в том, чтобы пробежаться по subviews у UICollectionView и починить руками сломанные фреймы. Оценить решение можно здесь. Выводы Так стоит ли использовать новый layout или нет? Как показала практика, технология пока не идеальна и имеет много нюансов и оговорок: на сложных интерфейсах можно столкнуться со множеством проблем, решение которых может заставить вас попотеть. Но, с другой стороны, можно вспомнить релиз UICollectionView в iOS 6 и сколько там было головной боли и проблем, но сейчас уже сложно представить жизнь без коллекций. Если говорить про наш проект, то переход к декларативному описанию лейаута, позволил нам сократить время разработки компонентов. Упростилась поддержка экрана, так как почти вся информация о вёрстке находится в одном месте и имеет общий вид (раньше приходилось скакать по xib-файлам и их классам, чтобы поменять какой-нибудь отступ и т.д.). Также теперь не должно возникать проблем со сложной версткой элементов (пламенный привет всем тем, кто занимался переопределением метода layoutAttributesForElements в UICollectionViewFlowLayout). Поэтому если у вас есть возможность и время, я настоятельно рекомендую начать использовать Compositional Layout в своих коллекциях и не бояться сопутствующих проблем. =========== Источник: habr.com =========== Похожие новости:
Блог компании Mail.ru Group ), #_blog_kompanii_delivery_club_tech ( Блог компании Delivery Club Tech ), #_razrabotka_pod_ios ( Разработка под iOS ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_interfejsy ( Интерфейсы ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:20
Часовой пояс: UTC + 5