🧩 iOS 自定义 UICollectionView 拼图布局 + 布局切换动画实践
本文只讲两件事: 👉 如何自定义 UICollectionView 布局 👉 如何优雅地切换布局并处理动画问题
✨ 一、为什么要自定义布局?
系统的 UICollectionViewFlowLayout 有一个明显限制:
👉 只能做规则网格(grid)布局
但很多场景需要:
- 拼图布局(不规则)
- 瀑布流变种
- 卡片混排
例如👇
┌───────────────┐
│ A │
├───────┬───────┤
│ B │ C │
└───────┴───────┘
👉 这种布局 FlowLayout 是做不了的
🧠 二、自定义布局核心原理
自定义布局本质只做三件事:
1️⃣ 计算每个 cell 的 frame
UICollectionViewLayoutAttributes
2️⃣ 返回可见区域的 attributes
override func layoutAttributesForElements(in rect: CGRect)
3️⃣ 告诉 collectionView 内容尺寸
override var collectionViewContentSize: CGSize
🔥 三、实现一个拼图布局(MosaicLayout)
核心代码
class MosaicLayout: UICollectionViewFlowLayout {
var layoutAttributes: [UICollectionViewLayoutAttributes] = []
var contentSize: CGSize = .zero
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
layoutAttributes.removeAll()
let count = collectionView.numberOfItems(inSection: 0)
guard count > 0 else { return }
let width = collectionView.bounds.width
let height = width
let frames: [CGRect] = [
CGRect(x: 0, y: 0, width: 1, height: 0.6),
CGRect(x: 0, y: 0.6, width: 0.5, height: 0.4),
CGRect(x: 0.5, y: 0.6, width: 0.5, height: 0.4)
]
for (index, relativeFrame) in frames.enumerated() {
let frame = CGRect(
x: relativeFrame.origin.x * width,
y: relativeFrame.origin.y * height,
width: relativeFrame.width * width,
height: relativeFrame.height * height
)
let attr = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: index, section: 0))
attr.frame = frame
layoutAttributes.append(attr)
}
contentSize = CGSize(width: width, height: height)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return layoutAttributes.filter { $0.frame.intersects(rect) }
}
override var collectionViewContentSize: CGSize {
return contentSize
}
}
🎬 五、布局切换动画
collectionView.setCollectionViewLayout(newLayout, animated: true)
⚠️ 六、崩溃问题
The invalidation context is not an instance of UICollectionViewFlowLayoutInvalidationContext
解决方案
collectionView.setCollectionViewLayout(newLayout, animated: false)
collectionView.reloadData()
🎯 七、总结
👉 自定义布局 = 自己控制 frame 👉 动画关键 = 避免同时更新数据源
如果你觉得有用,欢迎点赞 👍