paint-brush
Comment créer des listes déroulantes avec une conception orientée protocole et UICollectionViewCompositionalLayoutpar@bugorbn
701 lectures
701 lectures

Comment créer des listes déroulantes avec une conception orientée protocole et UICollectionViewCompositionalLayout

par Boris Bugor19m2024/06/18
Read on Terminal Reader

Trop long; Pour lire

La motivation de cette approche est très simple : nous voulons réduire la quantité de code passe-partout en créant des outils universels. Nous allons résoudre ce problème en 4 étapes. Écrire une abstraction du type de données des éléments défilants ; écrire une classe de base pour les éléments défilants ; Rédaction d'une implémentation pour les listes ; et Rédiger une implémentation pour les listes.
featured image - Comment créer des listes déroulantes avec une conception orientée protocole et UICollectionViewCompositionalLayout
Boris Bugor HackerNoon profile picture
0-item

Cet article est la suite de ma série sur l'utilisation d'une approche orientée protocole lors de la mise à l'échelle de projets avec une grande base de code.


Si vous n'avez pas lu le précédent article , je vous recommande fortement de vous familiariser avec les approches et les conclusions qui y sont formulées. En bref, un cas a été envisagé avec la création d'une classe universelle qui permettrait la création d'un constructeur pour utiliser des listes déroulantes basé sur UICollectionViewFlowLayout .


La motivation de cette approche est très simple : nous voulons réduire la quantité de code passe-partout en créant des outils universels qui réduiront la quantité de routine tout en ne perdant pas en flexibilité.


Dans cet article, nous continuerons à considérer une tâche similaire en utilisant UICollectionViewCompositionalLayout , pris en charge par iOS 13+, et voyez quelles nuances ce framework apporte.


Comme nous l'avons fait précédemment, nous allons résoudre ce problème en 4 étapes :


  1. Écrire une abstraction du type de données des éléments défilants ;
  2. Écrire une classe de base pour les éléments défilants ;
  3. Rédaction d'une implémentation pour les listes ;
  4. Cas d'utilisation


1. Éléments de défilement abstraits

La création de l’abstraction est sans aucun doute l’étape la plus importante du design. Pour jeter les bases d'un système ouvert à la mise à l'échelle, il est nécessaire de faire abstraction des caractéristiques qualitatives et quantitatives des éléments défilants. Il est également important de respecter les exigences pour le même type d'aménagement.


Introduisons une telle notion ; comme une section. Une section est un ou plusieurs éléments avec la même disposition.


Nous utilisons la section comme abstraction sur les éléments déroulants :

 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) }


Nous transférerons la responsabilité de la configuration de la mise en page à la section. La présence de vues supplémentaires, comme un en-tête ou un pied de page, y sera également déterminée.


2. Liste déroulante

La classe de base sera utilisée comme liste déroulante. La tâche de la classe de base est de prendre les données abstraites de la BaseSection et de les restituer. Dans notre cas, UICollectionView et UICollectionViewCompositionalFlowLayout seront utilisés comme outil de visualisation :


 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 , par rapport à l'utilisation de UICollectionViewFlowLayout , vous permet de transférer la configuration de la mise en page des cellules, des en-têtes et des pieds de page des méthodes déléguées vers le corps de la mise en page.

3. Implémentation d'éléments défilants

Étant donné que la section, qui inclut la possibilité d'afficher le pied de page et l'en-tête, a été considérée comme une abstraction, il est également nécessaire d'en tenir compte dans la classe d'implémentation.


Dans ce cas, les exigences pour n'importe quelle cellule ressembleront à ceci :

 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


On déplace la configuration de la taille de la cellule vers la zone de responsabilité de la cellule, on prend également en compte la possibilité de recevoir une action en tapant sur n'importe quelle cellule.


Les exigences en matière d’en-tête et de pied de page ressembleront à ceci :

 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? }


Sur la base des exigences relatives aux éléments de défilement, nous pouvons concevoir l'implémentation de la section :

 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?() } }


Les génériques qui implémentent leurs exigences agissent comme des types de cellule, d’en-tête ou de pied de page.


En général, l'implémentation est terminée, mais j'aimerais ajouter quelques assistants qui réduisent encore la quantité de code passe-partout. En particulier, en pratique, il ne sera pas toujours utile de disposer d’une telle section générique, pour la simple raison que le pied de page ou l’en-tête ne sont pas toujours utilisés.


Ajoutons ici une section qui prend en compte des cas similaires :

 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 } }


Sur ce point, la conception peut être considérée comme terminée, je propose de passer aux cas d'utilisation eux-mêmes.

4. Cas d'utilisation

Créons une section de cellules de taille fixe et affichons-la à l'écran :

 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 } }


Créons une implémentation de l'en-tête et du pied de page :

 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 } }


Ajoutons une nouvelle section à la liste déroulante :

 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 ) } }


Au total, en quelques lignes de code, nous avons implémenté une section de 5 cellules multicolores de taille proportionnelle à l'écran, un en-tête et un pied de page.




Essayons d'utiliser une approche similaire pour les cellules de taille dynamique.

 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 ) } }


En conséquence, nous nous sommes débarrassés de l'écriture de code passe-partout lors de la création de listes déroulantes basées sur UICollectionViewCompositionalLayout .



Le code source peut être consulté ici .


N'hésitez pas à me contacter au Twitter si vous avez des questions. De plus, vous pouvez toujours achète-moi un café .