paint-brush
Cách tạo danh sách có thể cuộn với thiết kế hướng giao thức & UICollectionViewCompositionalLayouttừ tác giả@bugorbn
701 lượt đọc
701 lượt đọc

Cách tạo danh sách có thể cuộn với thiết kế hướng giao thức & UICollectionViewCompositionalLayout

từ tác giả Boris Bugor19m2024/06/18
Read on Terminal Reader

dài quá đọc không nổi

Động lực của phương pháp này rất đơn giản, chúng tôi muốn giảm số lượng mã soạn sẵn bằng cách tạo ra các công cụ phổ quát. Chúng tôi sẽ giải quyết vấn đề này trong 4 giai đoạn. Viết bản tóm tắt kiểu dữ liệu của các phần tử cuộn; viết một lớp cơ sở cho các phần tử có thể cuộn được; Viết một triển khai cho danh sách; và Viết một triển khai cho Danh sách.
featured image - Cách tạo danh sách có thể cuộn với thiết kế hướng giao thức & UICollectionViewCompositionalLayout
Boris Bugor HackerNoon profile picture
0-item

Bài viết này là phần tiếp theo của loạt bài của tôi về cách sử dụng cách tiếp cận hướng giao thức khi mở rộng quy mô các dự án có cơ sở mã lớn.


Nếu bạn chưa đọc trước bài viết , tôi thực sự khuyên bạn nên tự làm quen với các cách tiếp cận và kết luận được đưa ra trong đó. Tóm lại, một trường hợp đã được xem xét với việc tạo một lớp phổ quát cho phép tạo một hàm tạo để sử dụng danh sách cuộn dựa trên UICollectionViewFlowLayout .


Động lực của phương pháp này rất đơn giản, chúng tôi muốn giảm số lượng mã soạn sẵn bằng cách tạo ra các công cụ phổ quát giúp giảm số lượng quy trình và đồng thời không mất đi tính linh hoạt.


Trong bài viết này, chúng ta sẽ tiếp tục xem xét một nhiệm vụ tương tự bằng cách sử dụng UICollectionViewCompositionalLayout , được hỗ trợ bởi iOS 13+ và xem khung này mang lại những sắc thái gì.


Như chúng tôi đã làm trước đây, chúng tôi sẽ giải quyết vấn đề này theo 4 giai đoạn:


  1. Viết bản tóm tắt kiểu dữ liệu của các phần tử cuộn;
  2. Viết một lớp cơ sở cho các phần tử có thể cuộn được;
  3. Viết một triển khai cho danh sách;
  4. Trường hợp sử dụng


1. Yếu tố cuộn trừu tượng

Việc tạo ra sự trừu tượng chắc chắn là giai đoạn quan trọng nhất của thiết kế. Để đặt nền tảng cho một hệ thống có thể mở rộng quy mô, cần phải trừu tượng hóa các đặc tính định tính và định lượng của các phần tử cuộn. Điều quan trọng là phải tuân thủ các yêu cầu đối với cùng một kiểu bố cục.


Hãy để chúng tôi giới thiệu một khái niệm như vậy; như một phần. Phần là một hoặc nhiều phần tử có cùng bố cục.


Chúng tôi sử dụng phần này dưới dạng trừu tượng hóa các phần tử có thể cuộn:

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


Chúng tôi sẽ chuyển trách nhiệm cấu hình bố cục cho phần này. Sự hiện diện của các chế độ xem bổ sung, chẳng hạn như đầu trang hoặc chân trang, cũng sẽ được xác định ở đó.


2. Danh sách cuộn

Lớp cơ sở sẽ được sử dụng làm danh sách có thể cuộn. Nhiệm vụ của lớp cơ sở là lấy dữ liệu trừu tượng của BaseSection và hiển thị nó. Trong trường hợp của chúng tôi, UICollectionViewUICollectionViewCompositionalFlowLayout sẽ được sử dụng làm công cụ trực quan hóa:


 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 , so với việc sử dụng UICollectionViewFlowLayout , cho phép bạn chuyển cấu hình bố cục ô, đầu trang và chân trang từ các phương thức ủy nhiệm sang nội dung bố cục.

3. Triển khai các phần tử có thể cuộn

Dựa trên thực tế là phần bao gồm khả năng hiển thị chân trang và tiêu đề, được coi là một phần trừu tượng, nên điều này cũng cần phải được tính đến trong lớp triển khai.


Trong trường hợp này, các yêu cầu đối với bất kỳ ô nào sẽ như sau:

 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


Chúng tôi di chuyển cấu hình kích thước ô đến khu vực chịu trách nhiệm của ô, chúng tôi cũng tính đến khả năng nhận được hành động bằng cách nhấn vào bất kỳ ô nào.


Các yêu cầu về đầu trang và chân trang sẽ như thế này:

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


Dựa trên yêu cầu về phần tử cuộn, chúng ta có thể thiết kế triển khai phần này:

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


Generics triển khai các yêu cầu cho chúng hoạt động như các loại ô, đầu trang hoặc chân trang.


Nói chung, quá trình triển khai đã hoàn tất, nhưng tôi muốn thêm một số trợ giúp để giảm thêm số lượng mã soạn sẵn. Đặc biệt, trong thực tế, không phải lúc nào cũng có một phần chung chung như vậy sẽ hữu ích, vì lý do đơn giản là phần chân trang hoặc phần đầu trang không phải lúc nào cũng được sử dụng.


Hãy thêm một phần ở đây có tính đến các trường hợp tương tự:

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


Về điều này, thiết kế có thể được coi là hoàn chỉnh, tôi đề xuất chuyển sang các trường hợp sử dụng.

4. Các trường hợp sử dụng

Hãy tạo một phần ô có kích thước cố định và hiển thị nó trên màn hình:

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


Hãy tạo một triển khai đầu trang và chân trang:

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


Hãy thêm một phần mới vào danh sách cuộn:

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


Tổng cộng, chỉ trong vài dòng mã, chúng tôi đã triển khai một phần gồm 5 ô nhiều màu với kích thước tỷ lệ thuận với màn hình, đầu trang và chân trang.




Hãy thử sử dụng cách tiếp cận tương tự cho các ô có kích thước động.

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


Kết quả là chúng tôi đã loại bỏ việc viết mã soạn sẵn khi tạo danh sách cuộn dựa trên UICollectionViewCompositionalLayout .



Mã nguồn có thể được xem đây .


Đừng ngần ngại liên hệ với tôi trên Twitter Nếu bạn có câu hỏi nào. Ngoài ra, bạn luôn có thể mua cho tôi một ly cà phê .