UICollectionView — 一个强大的iOS UIKit组件,用于以网格、列表和自定义布局的形式管理对象集合的显示。与UITableView不同,UICollectionView不局限于单列:它通过UICollectionViewLayout支持元素的任意排列。我们讲解UICollectionView的本质、FlowLayout和CompositionalLayout的工作原理,以及如何在Swift中创建自定义单元格。在IT Sectr,我们将UICollectionView用于画廊、目录和公告板。与表格比较,请阅读关于UITableView的文章。
要点
UICollectionView — 来自UIKit框架的一个类,于iOS 6(2012年)引入。它在由UICollectionViewLayout对象管理的灵活布局中显示有序的单元格(UICollectionViewCell)集合。UICollectionView解决了与UITableView(列表)相同的任务,但具有元素的任意排列:网格、轮播、拼贴、瀑布流。
架构 UICollectionView基于MVC模式构建:数据 — 在DataSource中,布局 — 在UICollectionViewLayout中,行为 — 在UICollectionViewDelegate中。这种分离允许在不更改数据代码的情况下切换布局。根据Apple(WWDC 2025),App Store前100名中72%的iOS应用使用UICollectionView来显示内容。CollectionView从iOS 6开始支持多列布局,从iOS 13开始获得了用于复杂部分的CompositionalLayout。
UICollectionView使用可重用单元格池(reuse queue),类似于UITableView。dequeueReusableCell(withReuseIdentifier:for:)方法从池中返回一个单元格,如果池为空则创建一个新单元格。与UITableView不同,UICollectionViewCell有一个内置的contentView,所有子视图都应添加到其中 — 不要直接将它们添加到单元格,因为contentView负责正确的选择动画和编辑动画。
UICollectionViewFlowLayout — 自iOS 6起工作的标准布局。它将元素按行(flow)排列,可设置单元格大小、滚动方向、最小间距和部分。UICollectionViewCompositionalLayout — 现代布局(iOS 13+),通过块的组合描述布局:组 → 部分 → 布局,其中每个组可以是水平、垂直或自定义的。
| 参数 | UICollectionViewFlowLayout | UICollectionViewCompositionalLayout |
|---|---|---|
| 最低版本 | iOS 6 | iOS 13 |
| 复杂布局 | 仅简单网格或列表 | 不同布局的部分、组、轮播 |
| 适应性 | 通过委托大小 | NSCollectionLayoutDimension.fractionalWidth/Height |
| 性能 | 渲染所有可见元素 | 延迟创建部分 |
| iPad支持 | 需要手动适配 | 通过fractionalWidth的自适应组 |
| 编写复杂度 | 低(5–10行) | 中(15–30行) |
// 带有FlowLayout的基本UICollectionView
class ViewController: UIViewController {
private var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumInteritemSpacing = 8
layout.minimumLineSpacing = 12
layout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
layout.itemSize = CGSize(width: view.frame.width - 32, height: 100)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "cell")
collectionView.dataSource = self
view.addSubview(collectionView)
}
}
// DataSource
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int { 20 }
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
as! CustomCell
cell.configure(with: "Item \(indexPath.row)")
return cell
}
}
Apple建议(WWDC 2025):对于新项目,使用CompositionalLayout。它为自适应布局提供了更多可能性,通过UICollectionView.Representable与UIKit和SwiftUI更好地协作,并支持自动大小的部分标题。将FlowLayout留给具有相同单元格网格和最低版本iOS 12及以下的简单屏幕。
UICollectionViewCell — UICollectionView单元格的基类。每个单元格包含contentView(主容器)、backgroundView(默认背景)、selectedBackgroundView(选中时背景)。在contentView内部调整单元格的外观 — 切勿直接将子视图添加到UICollectionViewCell。
// 带配置的自定义单元格
class ProductCell: UICollectionViewCell {
private let imageView = UIImageView()
private let titleLabel = UILabel()
private let priceLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
contentView.addSubview(imageView)
contentView.addSubview(titleLabel)
contentView.addSubview(priceLabel)
// Auto Layout constraints
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 8),
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
priceLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8)
])
}
func configure(with product: Product) {
imageView.image = UIImage(named: product.imageName)
titleLabel.text = product.title
priceLabel.text = product.price
}
}
UICollectionViewDelegate提供处理选择、高亮、上下文菜单和重新排序的方法。与UITableView不同,UICollectionViewDelegate不仅可以控制元素的点击,还可以控制长按(通过UIContextMenuInteraction的上下文菜单)。要处理点击,请实现collectionView(_:didSelectItemAt:) — 在选择动画完成后调用。
UICollectionViewDiffableDataSource — 类型安全的UICollectionViewDataSource替代品,于iOS 13中引入。无需通过cellForItemAt手动管理单元格,DiffableDataSource使用NSDiffableDataSourceSnapshot — 数据状态的快照,自动计算新旧状态之间的差异并动画显示更改。
// 带部分的DiffableDataSource
enum Section {
case featured, recommended, categories
}
class ModernDataSource: UIViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, Product>!
override func viewDidLoad() {
super.viewDidLoad()
configureDataSource()
applyInitialSnapshot()
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<ProductCell, Product> { cell, _, product in
cell.configure(with: product)
}
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
collectionView, indexPath, product in
collectionView.dequeueConfiguredReusableCell(
using: cellRegistration, for: indexPath, item: product
)
}
}
private func applyInitialSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, Product>()
snapshot.appendSections([.featured, .recommended, .categories])
snapshot.appendItems(featuredProducts, toSection: .featured)
snapshot.appendItems(recommendedProducts, toSection: .recommended)
snapshot.appendItems(categories, toSection: .categories)
dataSource.apply(snapshot, animatingDifferences: true)
}
// 自动动画更新
func updateProducts(_ products: [Product]) {
var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers)
snapshot.appendItems(products)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
DiffableDataSource的优势:(1)自动动画 — insert、delete、reload以默认动画进行;(2)类型安全 — 部分和元素是类型化的,编译错误而非运行时错误;(3)Snapshot — 状态快照可以保存、撤销、比较;(4)性能 — DiffableDataSource使用Hellir算法计算最小更改集。Apple(WWDC 2024)推荐对所有新的UICollectionView使用DiffableDataSource,旧的数据源仅保留给iOS 12及更早版本。
UICollectionViewLayout — 抽象类,完全控制集合元素的排列。如果FlowLayout和CompositionalLayout不适用(例如圆形布局、螺旋、图表),创建UICollectionViewLayout的子类并重写布局准备方法。
// 自定义圆形布局
class CircularLayout: UICollectionViewLayout {
private var attributesCache: [UICollectionViewLayoutAttributes] = []
private let radius: CGFloat = 120
override func prepare() {
super.prepare()
attributesCache.removeAll()
guard let collectionView else { return }
let count = collectionView.numberOfItems(inSection: 0)
let center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
for i in 0..<count {
let indexPath = IndexPath(item: i, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let angle = (CGFloat(i) / CGFloat(count)) * 2 * .pi
attributes.center = CGPoint(
x: center.x + radius * cos(angle),
y: center.y + radius * sin(angle)
)
attributes.size = CGSize(width: 60, height: 60)
attributesCache.append(attributes)
}
}
override var collectionViewContentSize: CGSize {
collectionView?.bounds.size ?? .zero
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
attributesCache.filter { $0.frame.intersects(rect) }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
attributesCache[indexPath.item]
}
}
性能:自定义UICollectionViewLayout应在prepare()中缓存属性,并在layoutAttributesForItem(at:)中以O(1)返回它们。对于1000+元素的集合,通过shouldInvalidateLayout(forBoundsChange:)使用增量缓存更新 — 这允许在滚动时不必重新计算整个布局。Apple(WWDC 2023)推荐CompositionalLayout而非自定义布局:它覆盖95%的场景,无需从头编写布局。
UITableView和UICollectionView — 用于显示列表的两个主要UIKit组件。UITableView更简单,专为单列列表设计。UICollectionView更灵活,支持任何布局。两者之间的选择取决于数据的视觉结构和所需的定制化。
| 参数 | UITableView | UICollectionView |
|---|---|---|
| 布局 | 仅垂直列表 | 列表、网格、瀑布流、轮播、自定义 |
| 复杂度 | 低(简单数据源) | 中(布局 + 数据源 + 委托) |
| 默认单元格 | 4种样式(basic、subtitle、value1、value2) | 仅空(通过contentView自定义) |
| 编辑模式 | 内置(delete、move、insert) | 需要手动实现 |
| 页眉/页脚 | 内置(viewForHeaderInSection) | 通过补充视图 |
| 性能 | 列表高 | 任何布局都高 |
| iOS 6+ | 从iOS 2可用 | 从iOS 6可用 |
实践规则:如果屏幕是简单的行列表(设置、联系人、菜单),使用UITableView。如果屏幕需要网格、水平滚动、轮播、不同单元格大小或复杂分组,使用UICollectionView。从iOS 14开始,UITableView也通过UICollectionLayoutListConfiguration获得了CompositionalLayout支持,这使得可以结合两个组件的优势。
常见问题
在UICollectionViewFlowLayout中设置scrollDirection = .horizontal。对于CompositionalLayout,使用带有fractionalWidth的NSCollectionLayoutSize来实现组的自适应宽度。水平滚动适用于轮播、画廊和分类。对于分页滚动,设置collectionView.isPagingEnabled = true或使用itemSize = bounds.width的UICollectionViewFlowLayout。
检查:(1)单元格已通过register(_:forCellWithReuseIdentifier:)注册,(2)DataSource返回的元素数量 > 0,(3)单元格使用正确的reuseIdentifier出队,(4)itemSize不为零(对于FlowLayout)。典型错误 — itemSize = CGSize.zero,导致单元格高度为零。显式设置itemSize或实现委托collectionView(_:layout:sizeForItemAt:)。
对于FlowLayout:layout.minimumInteritemSpacing(行内元素之间)和layout.minimumLineSpacing(行之间)。对于CompositionalLayout:NSCollectionLayoutGroup.interItemSpacing和组/部分上的contentInsets。sectionInset设置部分的外部间距。使用sectionInsetReference = .fromContentInset来正确考虑Safe Area。
使用performBatchUpdates进行组动画:collectionView.performBatchUpdates { insertItems, deleteItems, reloadItems }。对于安全和动画更新,使用UICollectionViewDiffableDataSource:调用dataSource.apply(snapshot, animatingDifferences: true) — 所有更改将自动计算并应用,无需手动insert/delete。
在SwiftUI中,UICollectionView的对应物是LazyVGrid和LazyHGrid(iOS 14+)。SwiftUI Grid在编写上更简单(声明式语法),但在500+元素和布局自定义方面不如UICollectionView。对于复杂集合,通过UIViewRepresentable使用UICollectionView。在IT Sectr项目中,我们为目录和画廊选择UICollectionView,为简单屏幕选择SwiftUI Grid。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。