如何在UICollectionViewCompositionalLayout中仅用指定单元格
问题描述
我开发了一个包含UICollectionViewCompositionalLayout及4种不同单元格的自定义库,现在要在新项目中引入该库,但仅需使用其中2种单元格。目前我通过实例化ShopViewController加载库代码,但不清楚如何在NewViewController中配置以仅启用指定的2种单元格。我希望后续项目只需导入所需单元格,无需重复编写单元格代码。
新项目的NewViewController代码:
import UIKit import MyTestLibrary class NewViewController: UIViewController { let shopViewController = ShopViewController() override func viewDidLoad() { super.viewDidLoad() shopViewController.createCollectionView() view.addSubview(shopViewController.collectionView) } // some code to use only 2 cell }
库的ShopViewController代码:
import UIKit public class ShopViewController: UIViewController { ///` create collection view public var collectionView: UICollectionView! ///` load data source from Section, Book files public var dataSource: UICollectionViewDiffableDataSource<Section, Book>? ///` load data from JSON file public let sections = Bundle.main.decode([Section].self, from: "appstore.json") public override func viewDidLoad() { super.viewDidLoad() createCollectionView() } public func createCollectionView() { collectionView = UICollectionView( frame: view.bounds, collectionViewLayout: createCompositionalLayout()) collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.backgroundColor = .systemBackground view.addSubview(collectionView) ///` register section header collectionView.register(SectionHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SectionHeader.reuseIdentifier) ///` register cells collectionView.register(FeaturedCell.self, forCellWithReuseIdentifier: FeaturedCell.reuseIdentifier) collectionView.register(MediumTableCell.self, forCellWithReuseIdentifier: MediumTableCell.reuseIdentifier) collectionView.register(SmallTableCell.self, forCellWithReuseIdentifier: SmallTableCell.reuseIdentifier) collectionView.register(ListTableCell.self, forCellWithReuseIdentifier: ListTableCell.reuseIdentifier) createDataSource() reloadData() } public func configure<T: SelfConfiguringCell>(_ cellType: T.Type, with book: Book, for indexPath: IndexPath) -> T { guard let cell = collectionView.dequeueReusableCell( withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else { fatalError("Unable to dequeue \(cellType)") } cell.configure(with: book) return cell } public func createDataSource() { dataSource = UICollectionViewDiffableDataSource<Section, Book>(collectionView: collectionView) { collectionView, indexPath, book in switch self.sections[indexPath.section].type { case "mediumTable": return self.configure(MediumTableCell.self, with: book, for: indexPath) case "smallTable": return self.configure(SmallTableCell.self, with: book, for: indexPath) case "listTable": return self.configure(ListTableCell.self, with: book, for: indexPath) default: return self.configure(FeaturedCell.self, with: book, for: indexPath) } } dataSource?.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in guard let sectionHeader = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: SectionHeader.reuseIdentifier, for: indexPath) as? SectionHeader else { return nil } guard let firstBook = self?.dataSource?.itemIdentifier( for: indexPath) else { return nil } guard let section = self?.dataSource?.snapshot().sectionIdentifier( containingItem: firstBook) else { return nil } if section.title.isEmpty { return nil } sectionHeader.title.text = section.title sectionHeader.subtitle.text = section.subtitle return sectionHeader } } public func reloadData() { var snapshot = NSDiffableDataSourceSnapshot<Section, Book>() snapshot.appendSections(sections) for section in sections { snapshot.appendItems(section.items, toSection: section) } dataSource?.apply(snapshot) } public func createCompositionalLayout() -> UICollectionViewLayout { let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in let section = self.sections[sectionIndex] switch section.type { case "mediumTable": return self.createMediumTableSection(using: section) case "smallTable": return self.createSmallTableSection(using: section) case "listTable": return self.createListTableSection(using: section) default: return self.createFeaturedSection(using: section) } } let config = UICollectionViewCompositionalLayoutConfiguration() config.interSectionSpacing = 20 layout.configuration = config return layout } public func createSectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem { let layoutSectionHeaderSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(0.93), heightDimension: .estimated(80)) let layoutSectionHeader = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: layoutSectionHeaderSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top) return layoutSectionHeader } public func createFeaturedSection(using section: Section) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize) layoutItem.contentInsets = NSDirectionalEdgeInsets( top: 0, leading: 5, bottom: 0, trailing: 5) let layoutGroupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(0.93), heightDimension: .estimated(288)) let layoutGroup = NSCollectionLayoutGroup.horizontal( layoutSize: layoutGroupSize, subitems: [layoutItem]) let layoutSection = NSCollectionLayoutSection(group: layoutGroup) layoutSection.orthogonalScrollingBehavior = .groupPagingCentered return layoutSection } public func createListTableSection(using section: Section) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalHeight(1)) let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize) layoutItem.contentInsets = NSDirectionalEdgeInsets( top: 8, leading: 8, bottom: 8, trailing: 8) let layoutGroupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(0.93), heightDimension: .fractionalWidth(0.5)) let layoutGroup = NSCollectionLayoutGroup.horizontal( layoutSize: layoutGroupSize, subitems: [layoutItem]) let layoutSection = NSCollectionLayoutSection(group: layoutGroup) layoutSection.orthogonalScrollingBehavior = .groupPagingCentered return layoutSection } public func createMediumTableSection(using section: Section) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.33)) let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize) layoutItem.contentInsets = NSDirectionalEdgeInsets( top: 0, leading: 5, bottom: 0, trailing: 5) let layoutGroupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(0.93), heightDimension: .fractionalWidth(0.55)) let layoutGroup = NSCollectionLayoutGroup.vertical( layoutSize: layoutGroupSize, subitems: [layoutItem]) let layoutSection = NSCollectionLayoutSection( group: layoutGroup) layoutSection.orthogonalScrollingBehavior = .groupPagingCentered let layoutSectionHeader = createSectionHeader() layoutSection.boundarySupplementaryItems = [layoutSectionHeader] return layoutSection } public func createSmallTableSection(using section: Section) -> NSCollectionLayoutSection { let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.2)) let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize) layoutItem.contentInsets = NSDirectionalEdgeInsets( top: 0, leading: 20, bottom: 0, trailing: 0) let layoutGroupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(0.93), heightDimension: .estimated(200)) let layoutGroup = NSCollectionLayoutGroup.vertical( layoutSize: layoutGroupSize, subitems: [layoutItem]) let layoutSection = NSCollectionLayoutSection( group: layoutGroup) let layoutSectionHeader = createSectionHeader() layoutSection.boundarySupplementaryItems = [layoutSectionHeader] return layoutSection } }
解决方案
要实现按需加载指定单元格,需要对库的ShopViewController做模块化改造,让外部可以指定允许使用的单元格类型,同时过滤对应的数据和布局逻辑。
步骤1:修改库代码,增加可配置项
首先在ShopViewController中添加枚举和公开配置属性,调整数据加载、单元格注册、数据源和布局逻辑:
import UIKit // 定义枚举对应所有支持的单元格类型 public enum CellType: String { case featured = "featured" case mediumTable = "mediumTable" case smallTable = "smallTable" case listTable = "listTable" } public class ShopViewController: UIViewController { ///` create collection view public var collectionView: UICollectionView! ///` load data source from Section, Book files public var dataSource: UICollectionViewDiffableDataSource<Section, Book>? /// 新增:允许外部配置需要启用的单元格类型,默认全部启用 public var allowedCellTypes: [CellType] = [.featured, .mediumTable, .smallTable, .listTable] ///` 过滤后的sections,只保留允许的类型 private var filteredSections: [Section] { return sections.filter { section in guard let type = CellType(rawValue: section.type) else { return false } return allowedCellTypes.contains(type) } } ///` load data from JSON file public let sections = Bundle.main.decode([Section].self, from: "appstore.json") public override func viewDidLoad() { super.viewDidLoad() createCollectionView() } public func createCollectionView() { collectionView = UICollectionView( frame: view.bounds, collectionViewLayout: createCompositionalLayout()) collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.backgroundColor = .systemBackground view.addSubview(collectionView) ///` register section header collectionView.register(SectionHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SectionHeader.reuseIdentifier) ///` register cells:只注册允许的单元格类型 for type in allowedCellTypes { switch type { case .featured: collectionView.register(FeaturedCell.self, forCellWithReuseIdentifier: FeaturedCell.reuseIdentifier) case .mediumTable: collectionView.register(MediumTableCell.self, forCellWithReuseIdentifier: MediumTableCell.reuseIdentifier) case .smallTable: collectionView.register(SmallTableCell.self, forCellWithReuseIdentifier: SmallTableCell.reuseIdentifier) case .listTable: collectionView.register(ListTableCell.self, forCellWithReuseIdentifier: ListTableCell.reuseIdentifier) } } createDataSource() reloadData() } public func configure<T: SelfConfiguringCell>(_ cellType: T.Type, with book: Book, for indexPath: IndexPath) -> T { guard let cell = collectionView.dequeueReusableCell( withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else { fatalError("Unable to dequeue \(cellType)") } cell.configure(with: book) return cell } public func createDataSource() { dataSource = UICollectionViewDiffableDataSource<Section, Book>(collectionView: collectionView) { collectionView, indexPath, book in let section = self.filteredSections[indexPath.section] guard let type = CellType(rawValue: section.type) else { return nil } switch type { case .mediumTable: return self.configure(MediumTableCell.self, with: book, for: indexPath) case .smallTable: return self.configure(SmallTableCell.self, with: book, for: indexPath) case .listTable: return self.configure(ListTableCell.self, with: book, for: indexPath) case .featured: return self.configure(FeaturedCell.self, with: book, for: indexPath) } } dataSource?.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in guard let sectionHeader = collectionView.dequeueReusableSupplementaryView( ofKind: kind, withReuseIdentifier: SectionHeader.reuseIdentifier, for: indexPath) as? SectionHeader else { return nil } guard let firstBook = self?.dataSource?.itemIdentifier( for: indexPath) else { return nil } guard let section = self?.dataSource?.snapshot().sectionIdentifier( containingItem: firstBook) else { return nil } if section.title.isEmpty { return nil } sectionHeader.title.text = section.title sectionHeader.subtitle.text = section.subtitle return sectionHeader } } public func reloadData() { var snapshot = NSDiffableDataSourceSnapshot<Section, Book>() snapshot.appendSections(filteredSections) for section in filteredSections { snapshot.appendItems(section.items, toSection: section) } dataSource?.apply(snapshot) } public func createCompositionalLayout() -> UICollectionViewLayout { let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in let section = self.filteredSections[sectionIndex] guard let type = CellType(rawValue: section.type) else { return nil } switch type { case .mediumTable: return self.createMediumTableSection(using: section) case .smallTable: return self.createSmallTableSection(using: section) case .listTable: return self.createListTableSection(using: section) case .featured: return self.createFeaturedSection(using: section) } } let config = UICollectionViewCompositionalLayoutConfiguration() config.interSectionSpacing = 20 layout.configuration = config return layout } // 以下方法保持原逻辑不变,省略重复代码 public func createSectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem { /* 原代码 */ } public func createFeaturedSection(using section: Section) -> NSCollectionLayoutSection { /* 原代码 */ } public func createListTableSection(using section: Section) -> NSCollectionLayoutSection { /* 原代码 */ } public func createMediumTableSection(using section: Section) -> NSCollectionLayoutSection { /* 原代码 */ } public func createSmallTableSection(using section: Section) -> NSCollectionLayoutSection { /* 原代码 */ } }
步骤2:在新项目中配置允许的单元格
在NewViewController中直接指定需要启用的单元格类型即可:
import UIKit import MyTestLibrary class NewViewController: UIViewController { let shopViewController = ShopViewController() override func viewDidLoad() { super.viewDidLoad() // 配置只启用mediumTable和featured两种单元格 shopViewController.allowedCellTypes = [.mediumTable, .featured] shopViewController.createCollectionView() view.addSubview(shopViewController.collectionView) // 可选:设置约束替代autoresizingMask,适配不同屏幕 shopViewController.collectionView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ shopViewController.collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), shopViewController.collectionView.leadingAnchor.constraint(equalTo: view.
相关产品推荐
相关产品推荐

