Bu makale, geniş kod tabanına sahip projeleri ölçeklendirirken protokol odaklı bir yaklaşım kullanmaya ilişkin serimin devamı niteliğindedir.
Eğer okumadıysanızUICollectionViewFlowLayout
dayalı kayan listeleri kullanmak için bir kurucunun oluşturulmasına izin verecek evrensel bir sınıfın oluşturulmasıyla ilgili bir durum değerlendirildi.
Bu yaklaşımın motivasyonu çok basit; rutinin miktarını azaltacak ve aynı zamanda esnekliği kaybetmeyecek evrensel araçlar yaratarak standart kod miktarını azaltmak istiyoruz.
Bu yazıda benzer bir görevi ele almaya devam edeceğiz.
Daha önce yaptığımız gibi bu sorunu 4 aşamada çözeceğiz:
Soyutlamanın yaratılması şüphesiz tasarımın en önemli aşamasıdır. Ölçeklendirmeye açık bir sistemin temelini atmak için kaydırma elemanlarının niteliksel ve niceliksel özelliklerinden soyutlamak gerekir. Aynı tür düzen için gerekliliklere uymak da önemlidir.
Böyle bir kavramı ortaya koyalım; bölüm olarak. Bölüm, aynı düzene sahip bir veya daha fazla öğedir.
Bu bölümü kaydırılabilir öğeler üzerinde bir soyutlama olarak kullanıyoruz:
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) }
Düzeni yapılandırma sorumluluğunu bölüme devredeceğiz. Üstbilgi veya altbilgi gibi ek görünümlerin varlığı da burada belirlenecektir.
Kaydırılabilir liste olarak temel sınıf kullanılacaktır. Temel sınıfın görevi, BaseSection'ın soyut verilerini alıp onu oluşturmaktır. Bizim durumumuzda görselleştirme aracı olarak UICollectionView
ve UICollectionViewCompositionalFlowLayout
kullanılacaktır:
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
, UICollectionViewFlowLayout
kullanımına kıyasla hücre, üstbilgi ve altbilgi düzeni yapılandırmasını temsilci yöntemlerinden düzen gövdesine aktarmanıza olanak tanır.
Altbilgi ve üstbilgi gösterme özelliğinin yer aldığı bölümün soyutlama olarak alındığı gerçeğinden hareketle uygulama sınıfında bunun da dikkate alınması gerekir.
Bu durumda herhangi bir hücrenin gereksinimleri şöyle görünecektir:
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
Hücre boyutunun konfigürasyonunu hücrenin sorumluluk alanına taşıyoruz, ayrıca herhangi bir hücreye dokunarak işlem alma olasılığını da hesaba katıyoruz.
Üstbilgi ve altbilgi gereksinimleri şöyle görünecektir:
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? }
Kaydırma elemanlarının gereksinimlerine dayanarak bölümün uygulamasını tasarlayabiliriz:
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?() } }
Kendilerine yönelik gereksinimleri uygulayan jenerikler, hücre, üstbilgi veya altbilgi türleri olarak işlev görür.
Genel olarak uygulama tamamlandı ancak standart kod miktarını daha da azaltacak birkaç yardımcı eklemek istiyorum. Özellikle pratikte, altbilgi veya başlığın her zaman kullanılmaması gibi basit bir nedenden ötürü, bu tür genel bir bölüme sahip olmak her zaman yararlı olmayacaktır.
Buraya benzer durumları dikkate alan bir bölüm ekleyelim:
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 } }
Bu konuda tasarımın tamamlanmış olduğu düşünülebilir, kullanım senaryolarına geçmeyi öneriyorum.
Sabit boyutta bir hücre bölümü oluşturalım ve bunu ekranda gösterelim:
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 } }
Üstbilgi ve altbilginin bir uygulamasını oluşturalım:
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 } }
Kaydırma listesine yeni bir bölüm ekleyelim:
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 ) } }
Toplamda, yalnızca birkaç satır kodla, boyutu ekranla orantılı, üstbilgi ve altbilgiye sahip 5 adet çok renkli hücreden oluşan bir bölüm uyguladık.
Dinamik olarak boyutlandırılmış hücreler için benzer bir yaklaşım kullanmaya çalışalım.
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 ) } }
Sonuç olarak, UICollectionViewCompositionalLayout
dayalı kayan listeler oluştururken standart kod yazmaktan kurtulduk.
Kaynak kodu görüntülenebilir
Benimle iletişime geçmekten çekinmeyin