如何在UICollectionViewDiffableDataSource中实现多类型Item集合视图?
用UICollectionViewDiffableDataSource实现多分区多Item类型的集合视图
1. 定义分区与Item枚举
先创建对应4个分区的枚举,再通过带关联值的Item枚举绑定各分区对应的数据模型,两者都需遵守Hashable协议,这是DiffableDataSource的核心要求。
// 对应4个分区的枚举 enum CollectionSection: Hashable { case banner case featuredProducts case categoryList case recommendedItems } // 绑定各分区数据模型的Item枚举 enum CollectionItem: Hashable { case bannerItem(BannerModel) case productItem(ProductModel) case categoryItem(CategoryModel) case recommendedItem(RecommendedModel) // 手动实现Hashable,基于关联模型的唯一标识做哈希 func hash(into hasher: inout Hasher) { switch self { case .bannerItem(let model): hasher.combine(model.id) case .productItem(let model): hasher.combine(model.productId) case .categoryItem(let model): hasher.combine(model.categoryId) case .recommendedItem(let model): hasher.combine(model.itemId) } } // 手动实现相等判断,确保同类型Item的唯一性 static func == (lhs: CollectionItem, rhs: CollectionItem) -> Bool { switch (lhs, rhs) { case (.bannerItem(let lhsModel), .bannerItem(let rhsModel)): return lhsModel.id == rhsModel.id case (.productItem(let lhsModel), .productItem(let rhsModel)): return lhsModel.productId == rhsModel.productId case (.categoryItem(let lhsModel), .categoryItem(let rhsModel)): return lhsModel.categoryId == rhsModel.categoryId case (.recommendedItem(let lhsModel), .recommendedItem(let rhsModel)): return lhsModel.itemId == rhsModel.itemId default: return false } } } // 示例数据模型,各自遵守Hashable struct BannerModel: Hashable { let id: String let imageURL: String } struct ProductModel: Hashable { let productId: String let name: String let price: Double } struct CategoryModel: Hashable { let categoryId: String let title: String } struct RecommendedModel: Hashable { let itemId: String let description: String }
2. 创建并配置DiffableDataSource
初始化数据源时指定分区和Item类型,在cellProvider闭包中根据Item类型匹配对应Cell并配置数据。同时完成CollectionView的布局和Cell注册。
class YourViewController: UIViewController { private var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource<CollectionSection, CollectionItem>! override func viewDidLoad() { super.viewDidLoad() setupCollectionView() setupDataSource() loadInitialData() } private func setupCollectionView() { // 用CompositionalLayout配置各分区布局(可替换为其他布局) let layout = UICollectionViewCompositionalLayout { sectionIndex, _ in guard let section = CollectionSection(rawValue: sectionIndex) else { return NSCollectionLayoutSection() } switch section { case .banner: // 全屏横幅布局 let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))) let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(200)), subitems: [item]) return NSCollectionLayoutSection(group: group) case .featuredProducts: // 双列商品网格布局 let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1))) item.contentInsets = .init(top: 8, leading: 8, bottom: 8, trailing: 8) let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(250)), subitems: [item, item]) return NSCollectionLayoutSection(group: group) case .categoryList: // 横向滚动分类布局 let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .absolute(100), heightDimension: .absolute(100))) item.contentInsets = .init(top: 8, leading: 8, bottom: 8, trailing: 8) let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .estimated(100), heightDimension: .absolute(100)), subitems: [item]) let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .continuous return section case .recommendedItems: // 纵向列表推荐布局 let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(80))) item.contentInsets = .init(top: 4, leading: 8, bottom: 4, trailing: 8) let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(80)), subitems: [item]) return NSCollectionLayoutSection(group: group) } } collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] // 注册所有类型的Cell collectionView.register(BannerCell.self, forCellWithReuseIdentifier: BannerCell.reuseIdentifier) collectionView.register(ProductCell.self, forCellWithReuseIdentifier: ProductCell.reuseIdentifier) collectionView.register(CategoryCell.self, forCellWithReuseIdentifier: CategoryCell.reuseIdentifier) collectionView.register(RecommendedCell.self, forCellWithReuseIdentifier: RecommendedCell.reuseIdentifier) view.addSubview(collectionView) } private func setupDataSource() { dataSource = UICollectionViewDiffableDataSource<CollectionSection, CollectionItem>(collectionView: collectionView) { collectionView, indexPath, item in switch item { case .bannerItem(let model): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BannerCell.reuseIdentifier, for: indexPath) as! BannerCell cell.configure(with: model) return cell case .productItem(let model): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProductCell.reuseIdentifier, for: indexPath) as! ProductCell cell.configure(with: model) return cell case .categoryItem(let model): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CategoryCell.reuseIdentifier, for: indexPath) as! CategoryCell cell.configure(with: model) return cell case .recommendedItem(let model): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RecommendedCell.reuseIdentifier, for: indexPath) as! RecommendedCell cell.configure(with: model) return cell } } } private func loadInitialData() { // 创建快照并填充数据 var snapshot = NSDiffableDataSourceSnapshot<CollectionSection, CollectionItem>() snapshot.appendSections([.banner, .featuredProducts, .categoryList, .recommendedItems]) // 模拟各分区数据 let banners = [BannerModel(id: "b1", imageURL: "banner1"), BannerModel(id: "b2", imageURL: "banner2")] snapshot.appendItems(banners.map { CollectionItem.bannerItem($0) }, toSection: .banner) let products = [ProductModel(productId: "p1", name: "商品1", price: 99.9), ProductModel(productId: "p2", name: "商品2", price: 199.9)] snapshot.appendItems(products.map { CollectionItem.productItem($0) }, toSection: .featuredProducts) let categories = [CategoryModel(categoryId: "c1", title: "分类1"), CategoryModel(categoryId: "c2", title: "分类2"), CategoryModel(categoryId: "c3", title: "分类3")] snapshot.appendItems(categories.map { CollectionItem.categoryItem($0) }, toSection: .categoryList) let recommendeds = [RecommendedModel(itemId: "r1", description: "推荐商品1"), RecommendedModel(itemId: "r2", description: "推荐商品2")] snapshot.appendItems(recommendeds.map { CollectionItem.recommendedItem($0) }, toSection: .recommendedItems) // 应用快照,自动更新UI dataSource.apply(snapshot, animatingDifferences: true) } }
3. 自定义Cell示例
每个Cell实现对应的数据配置方法,接收并展示对应模型的数据:
class BannerCell: UICollectionViewCell { static let reuseIdentifier = "BannerCell" private let imageView = UIImageView() override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupUI() { imageView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(imageView) imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true NSLayoutConstraint.activate([ imageView.topAnchor.constraint(equalTo: contentView.topAnchor), imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) ]) } func configure(with model: BannerModel) { // 实际项目中用图片加载框架加载URL,这里用占位色示例 imageView.backgroundColor = .lightGray } } class ProductCell: UICollectionViewCell { static let reuseIdentifier = "ProductCell" private let nameLabel = UILabel() private let priceLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupUI() { contentView.backgroundColor = .white contentView.layer.cornerRadius = 8 contentView.clipsToBounds = true nameLabel.translatesAutoresizingMaskIntoConstraints = false priceLabel.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(nameLabel) contentView.addSubview(priceLabel) nameLabel.font = .systemFont(ofSize: 14) priceLabel.font = .systemFont(ofSize: 13, weight: .bold) priceLabel.textColor = .red NSLayoutConstraint.activate([ nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8), nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8), priceLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8), priceLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8), priceLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8) ]) } func configure(with model: ProductModel) { nameLabel.text = model.name priceLabel.text = String(format: "¥%.2f", model.price) } } // CategoryCell和RecommendedCell可参照上述实现,绑定对应模型即可 class CategoryCell: UICollectionViewCell { static let reuseIdentifier = "CategoryCell" private let titleLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupUI() { titleLabel.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(titleLabel) contentView.backgroundColor = .white contentView.layer.cornerRadius = 8 titleLabel.textAlignment = .center NSLayoutConstraint.activate([ titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) ]) } func configure(with model: CategoryModel) { titleLabel.text = model.title } } class RecommendedCell: UICollectionViewCell { static let reuseIdentifier = "RecommendedCell" private let descLabel = UILabel() override init(frame: CGRect) { super.init(frame: frame) setupUI() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupUI() { descLabel.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(descLabel) contentView.backgroundColor = .white contentView.layer.cornerRadius = 8 NSLayoutConstraint.activate([ descLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), descLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) ]) } func configure(with model: RecommendedModel) { descLabel.text = model.description } }
关键注意点
- Hashable实现:带关联值的枚举必须手动实现
Hashable,确保DiffableDataSource能正确识别Item的变化,避免重复或错误更新。 - Cell类型匹配:在
cellProvider闭包中通过switch精准匹配Item类型,避免强制类型转换错误。 - 快照更新:后续数据更新只需创建新快照并调用
apply方法,DiffableDataSource会自动处理UI的增删改动画,无需手动操作索引路径。
内容的提问来源于stack exchange,提问作者Mobin Jahantark
相关产品推荐
相关产品推荐

