使用UICollectionViewCompositionalLayout搭配DiffableDataSource如何自定义cell插入删除动画
问题原因
UICollectionViewCompositionalLayout 默认内置了插入删除的淡入淡出动画优先级,会覆盖你重写的initialLayoutAttributesForAppearingItem和finalLayoutAttributesForDisappearingItem的返回值,同时DiffableDataSource应用快照时的动画逻辑会优先遵从layout的内置配置,导致原有自定义动画失效。
实现方案
你可以选择以下两种方案实现自定义动画,可根据业务复杂度选择:
方案1:子类化UICollectionViewCompositionalLayout 兼容原有重写逻辑
该方案可以复用你之前写的自定义属性逻辑,改动量最小:
- 子类化
UICollectionViewCompositionalLayout,保留你原有对initialLayoutAttributesForAppearingItem、finalLayoutAttributesForDisappearingItem的重写逻辑 - 新增重写
shouldInvalidateLayout(forPreferredLayoutAttributes:withOriginalAttributes:)方法返回true,确保你自定义的布局属性不会被系统默认配置覆盖 - 应用快照时保持
apply(snapshot, animatingDifferences: true)的动画参数为true,不要关闭全局动画开关
示例代码:
class CustomCompositionalLayout: UICollectionViewCompositionalLayout { // 重写item出现时的初始属性 override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { guard let attributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath)?.copy() as? UICollectionViewLayoutAttributes else { return nil } // 替换为你自定义的动画初始状态,以下为缩放+上移示例 attributes.alpha = 0 attributes.transform = CGAffineTransform(scaleX: 0.5, y: 0.5).translatedBy(x: 0, y: 100) return attributes } // 重写item消失时的最终属性 override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { guard let attributes = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath)?.copy() as? UICollectionViewLayoutAttributes else { return nil } // 替换为你自定义的动画结束状态,以下为缩放+下移示例 attributes.alpha = 0 attributes.transform = CGAffineTransform(scaleX: 0.5, y: 0.5).translatedBy(x: 0, y: -100) return attributes } // 新增该重写方法,保证自定义属性生效 override func shouldInvalidateLayout(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> Bool { return true } }
使用时直接用该自定义子类替换原来的UICollectionViewCompositionalLayout初始化即可。
方案2:无需子类化Layout,通过Cell生命周期方法实现
该方案适合简单动画场景,不需要修改原有layout配置:
- 维护一个集合存储正在执行动画的indexPath,避免cell复用导致的动画重复或错乱
- 在
collectionView(_:willDisplay:forItemAt:)代理方法中设置cell的动画初始状态,再通过UIView动画复位到正常状态 - 若需要自定义删除动画,可配合
collectionView(_:didEndDisplaying:forItemAt:)方法实现
示例代码:
// 全局存储正在执行动画的indexPath,避免复用问题 private var animatingIndexPaths = Set<IndexPath>() func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { guard !animatingIndexPaths.contains(indexPath) else { return } animatingIndexPaths.insert(indexPath) // 动画初始状态 cell.alpha = 0 cell.transform = CGAffineTransform(translationX: 0, y: 50) UIView.animate(withDuration: 0.3) { // 复位到正常状态 cell.alpha = 1 cell.transform = .identity } completion: { [weak self] _ in self?.animatingIndexPaths.remove(indexPath) } }
注意事项
- 无论使用哪种方案,应用DiffableDataSource快照时如果设置
animatingDifferences: false,所有自定义动画都不会生效 - 方案2中页面销毁时记得清空
animatingIndexPaths,避免内存泄露
内容的提问来源于stack exchange,提问作者tadija
相关产品推荐
相关产品推荐

