You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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.
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 23:22:29