paint-brush
Como criar listas roláveis com design orientado a protocolo e UICollectionViewCompositionalLayoutpor@bugorbn
656 leituras
656 leituras

Como criar listas roláveis com design orientado a protocolo e UICollectionViewCompositionalLayout

por Boris Bugor19m2024/06/18
Read on Terminal Reader

Muito longo; Para ler

A motivação para esta abordagem é muito simples: queremos reduzir a quantidade de código clichê criando ferramentas universais. Resolveremos este problema em 4 etapas. Escrever uma abstração do tipo de dados dos elementos de rolagem; escrever uma classe base para elementos roláveis; Escrevendo uma implementação para listas; e Escrevendo uma implementação para Listas.
featured image - Como criar listas roláveis com design orientado a protocolo e UICollectionViewCompositionalLayout
Boris Bugor HackerNoon profile picture
0-item

Este artigo é uma continuação da minha série sobre como usar uma abordagem orientada a protocolo ao dimensionar projetos com uma grande base de código.


Se você não leu o anterior artigo , recomendo fortemente que você se familiarize com as abordagens e conclusões nele contidas. Resumidamente, foi considerado um caso com a criação de uma classe universal que permitiria a criação de um construtor para utilização de listas de rolagem baseado em UICollectionViewFlowLayout .


A motivação para esta abordagem é muito simples: queremos reduzir a quantidade de código clichê criando ferramentas universais que irão reduzir a quantidade de rotina e ao mesmo tempo não perder flexibilidade.


Neste artigo, continuaremos a considerar uma tarefa semelhante usando UICollectionViewCompositionalLayout , compatível com iOS 13+, e veja quais nuances esse framework traz.


Como fizemos anteriormente, resolveremos este problema em 4 etapas:


  1. Escrever uma abstração do tipo de dados dos elementos de rolagem;
  2. Escrevendo uma classe base para elementos roláveis;
  3. Escrevendo uma implementação para listas;
  4. Casos de uso


1. Elementos abstratos de rolagem

A criação da abstração é sem dúvida a etapa mais importante do design. Para estabelecer as bases de um sistema aberto ao escalonamento, é necessário abstrair das características qualitativas e quantitativas dos elementos de rolagem. Também é importante cumprir os requisitos para o mesmo tipo de layout.


Vamos apresentar tal noção; como uma seção. Uma seção é um ou mais elementos com o mesmo layout.


Usamos a seção como uma abstração sobre os elementos roláveis:

 protocol BaseSection { var numberOfElements: Int { get } func registrate(collectionView: UICollectionView) func cell(for collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell func header(for collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionReusableView func footer(for collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionReusableView func section() -> NSCollectionLayoutSection func select(row: Int) }


Transferiremos a responsabilidade pela configuração do layout para a seção. A presença de visualizações complementares, como cabeçalho ou rodapé, também será determinada aí.


2. Lista de rolagem

A classe base será usada como lista rolável. A tarefa da classe base é pegar os dados abstratos de BaseSection e renderizá-los. No nosso caso, UICollectionView e UICollectionViewCompositionalFlowLayout serão usados como ferramenta de visualização:


 class SectionView: UIView { override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.frame = bounds addSubview(collectionView) } private(set) lazy var flowLayout: UICollectionViewCompositionalLayout = { let layout = UICollectionViewCompositionalLayout { [weak self] index, env in self?.sections[index].section() } return layout }() private(set) lazy var collectionView: UICollectionView = { let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) collectionView.backgroundColor = .clear collectionView.delegate = self collectionView.dataSource = self return collectionView }() private var sections: [BaseSection] = [] public func set(sections: [BaseSection], append: Bool) { sections.forEach { $0.registrate(collectionView: collectionView) } if append { self.sections.append(contentsOf: sections) } else { self.sections = sections } collectionView.reloadData() } public func set(contentInset: UIEdgeInsets) { collectionView.contentInset = contentInset } } extension SectionView: UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { sections.count } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { sections[section].numberOfElements } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { sections[indexPath.section].cell(for: collectionView, indexPath: indexPath) } func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { kind == UICollectionView.elementKindSectionHeader ? sections[indexPath.section].header(for: collectionView, indexPath: indexPath) : sections[indexPath.section].footer(for: collectionView, indexPath: indexPath) } } extension SectionView: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { sections[indexPath.section].select(row: indexPath.row) } }


UICollectionViewCompositionalLayout , comparado ao uso de UICollectionViewFlowLayout , permite transferir a configuração de layout de célula, cabeçalho e rodapé de métodos delegados para o corpo do layout.

3. Implementando Elementos Roláveis

Pelo fato de a seção, que inclui a possibilidade de mostrar rodapé e cabeçalho, ter sido tomada como uma abstração, também é necessário levar isso em consideração na classe de implementação.


Neste caso, os requisitos para qualquer célula serão assim:

 protocol SectionCell: UICollectionViewCell { associatedtype CellData: SectionCellData func setup(with data: CellData) -> Self static func groupSize() -> NSCollectionLayoutGroup } protocol SectionCellData { var onSelect: VoidClosure? { get } } typealias VoidClosure = () -> Void


Movemos a configuração do tamanho da célula para a área de responsabilidade da célula, também levamos em consideração a possibilidade de receber uma ação tocando em qualquer célula.


Os requisitos de cabeçalho e rodapé serão assim:

 protocol SectionHeader: UICollectionReusableView { associatedtype HeaderData func setup(with data: HeaderData?) -> Self static func headerItem() -> NSCollectionLayoutBoundarySupplementaryItem? } protocol SectionFooter: UICollectionReusableView { associatedtype FooterData func setup(with data: FooterData?) -> Self static func footerItem() -> NSCollectionLayoutBoundarySupplementaryItem? }


Com base nos requisitos para elementos de rolagem, podemos projetar a implementação da seção:

 class Section<Cell: SectionCell, Header: SectionHeader, Footer: SectionFooter>: BaseSection { init(items: [Cell.CellData], headerData: Header.HeaderData? = nil, footerData: Footer.FooterData? = nil) { self.items = items self.headerData = headerData self.footerData = footerData } private(set) var items: [Cell.CellData] private let headerData: Header.HeaderData? private let footerData: Footer.FooterData? var numberOfElements: Int { items.count } func registrate(collectionView: UICollectionView) { collectionView.register(Cell.self) collectionView.registerHeader(Header.self) collectionView.registerFooter(Footer.self) } func cell(for collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { collectionView .dequeue(Cell.self, indexPath: indexPath)? .setup(with: items[indexPath.row]) ?? UICollectionViewCell() } func header(for collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionReusableView { collectionView .dequeueHeader(Header.self, indexPath: indexPath)? .setup(with: headerData) ?? UICollectionReusableView() } func footer(for collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionReusableView { collectionView .dequeueFooter(Footer.self, indexPath: indexPath)? .setup(with: footerData) ?? UICollectionReusableView() } func section() -> NSCollectionLayoutSection { let section = NSCollectionLayoutSection(group: Cell.groupSize()) if let headerItem = Header.headerItem() { section.boundarySupplementaryItems.append(headerItem) } if let footerItem = Footer.footerItem() { section.boundarySupplementaryItems.append(footerItem) } return section } func select(row: Int) { items[row].onSelect?() } }


Os genéricos que implementam os requisitos para eles atuam como tipos de célula, cabeçalho ou rodapé.


Em geral, a implementação está completa, mas gostaria de adicionar alguns auxiliares que reduzem ainda mais a quantidade de código padrão. Em particular, na prática, nem sempre será útil ter uma secção tão genérica, pela simples razão de que o rodapé ou cabeçalho nem sempre é utilizado.


Vamos adicionar aqui uma seção que leva em consideração casos semelhantes:

 class SectionWithoutHeaderFooter<Cell: SectionCell>: Section<Cell, EmptySectionHeader, EmptySectionFooter> {} class EmptySectionHeader: UICollectionReusableView, SectionHeader { func setup(with data: String?) -> Self { self } static func headerItem() -> NSCollectionLayoutBoundarySupplementaryItem? { nil } } class EmptySectionHeader: UICollectionReusableView, SectionHeader { func setup(with data: String?) -> Self { self } static func headerItem() -> NSCollectionLayoutBoundarySupplementaryItem? { nil } }


Com isso, o design pode ser considerado completo, proponho passar para os próprios casos de uso.

4. Casos de uso

Vamos criar uma seção de células com tamanho fixo e exibi-la na tela:

 class ColorCollectionCell: UICollectionViewCell, SectionCell { func setup(with data: ColorCollectionCellData) -> Self { contentView.backgroundColor = data.color return self } static func groupSize() -> NSCollectionLayoutGroup { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.5)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitem: item, count: 2) group.interItemSpacing = .fixed(16) return group } } class ColorCollectionCellData: SectionCellData { let onSelect: VoidClosure? let color: UIColor init(color: UIColor, onSelect: VoidClosure? = nil) { self.onSelect = onSelect self.color = color } }


Vamos criar uma implementação de cabeçalho e rodapé:

 class DefaultSectionHeader: UICollectionReusableView, SectionHeader { let textLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 32, weight: .bold) return label }() override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { addSubview(textLabel) textLabel.numberOfLines = .zero textLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ textLabel.topAnchor.constraint(equalTo: topAnchor), textLabel.bottomAnchor.constraint(equalTo: bottomAnchor), textLabel.leftAnchor.constraint(equalTo: leftAnchor), textLabel.rightAnchor.constraint(equalTo: rightAnchor) ]) } func setup(with data: String?) -> Self { textLabel.text = data return self } static func headerItem() -> NSCollectionLayoutBoundarySupplementaryItem? { let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(20)) let header = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top, absoluteOffset: .zero ) header.pinToVisibleBounds = true return header } } class DefaultSectionFooter: UICollectionReusableView, SectionFooter { let textLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 12, weight: .light) return label }() override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { addSubview(textLabel) textLabel.numberOfLines = .zero textLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ textLabel.topAnchor.constraint(equalTo: topAnchor), textLabel.bottomAnchor.constraint(equalTo: bottomAnchor), textLabel.leftAnchor.constraint(equalTo: leftAnchor), textLabel.rightAnchor.constraint(equalTo: rightAnchor) ]) } func setup(with data: String?) -> Self { textLabel.text = data return self } static func footerItem() -> NSCollectionLayoutBoundarySupplementaryItem? { let footerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(20)) let footer = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: footerSize, elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottom, absoluteOffset: .zero ) return footer } }


Vamos adicionar uma nova seção à lista de rolagem:

 class ViewController: UIViewController { let sectionView = SectionView() override func loadView() { view = sectionView } override func viewDidLoad() { super.viewDidLoad() sectionView.backgroundColor = .white sectionView.set( sections: [ Section<ColorCollectionCell, DefaultSectionHeader, DefaultSectionFooter>( items: [ .init(color: .blue) { print(#function) }, .init(color: .red) { print(#function) }, .init(color: .yellow) { print(#function) }, .init(color: .green) { print(#function) }, .init(color: .blue) { print(#function) } ], headerData: "COLOR SECTION", footerData: "footer text for color section" ) ], append: false ) } }


No total, em apenas algumas linhas de código, implementamos uma seção de 5 células multicoloridas com tamanho proporcional à tela, um cabeçalho e um rodapé.




Vamos tentar usar uma abordagem semelhante para células de tamanho dinâmico.

 class DynamicCollectionCell: UICollectionViewCell, SectionCell { let textLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } private func commonInit() { contentView.addSubview(textLabel) textLabel.numberOfLines = .zero textLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ textLabel.topAnchor.constraint(equalTo: contentView.topAnchor), textLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), textLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor), textLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor) ]) } func setup(with data: DynamicCollectionCellData) -> Self { textLabel.text = data.text return self } static func groupSize() -> NSCollectionLayoutGroup { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(20)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item]) return group } } class DynamicCollectionCellData: SectionCellData { let text: String var onSelect: VoidClosure? init(text: String) { self.text = text } } class ViewController: UIViewController { ... override func viewDidLoad() { super.viewDidLoad() ... sectionView.set( sections: [ SectionWithoutHeaderFooter<DynamicCollectionCell>( items: [ .init(text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s"), .init(text: "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged."), .init(text: "It was popularised"), .init(text: "the 1960s with the release of Letraset sheets containing"), .init(text: "Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.") ] ), ... ], append: false ) } }


Como resultado, nos livramos de escrever código clichê ao criar listas de rolagem com base em UICollectionViewCompositionalLayout .



O código fonte pode ser visualizado aqui .


Não hesite em contactar-me em Twitter se você tiver quaisquer perguntas. Além disso, você sempre pode me compre um café .