iOS 16 Beta下UICollectionView拖动Item时崩溃问题排查
问题原因及解决方案
原因分析
- iOS 16 交互逻辑变更:苹果在iOS 16中调整了UICollectionView交互拖动时的布局无效化流程,在拖动的某些中间阶段(比如刚触发拖动尚未产生实际位置变化、系统内部预校验步骤),
previousIndexPaths和targetIndexPaths会传入空数组,而非iOS 15及更早版本中始终传入的非空数组。你代码中直接使用previousIndexPaths.first!、targetIndexPaths.first!这种强制解包操作,空数组的first会返回nil,强制解包就会触发崩溃。 - 职责越界的代码逻辑:你在
UICollectionViewLayout的invalidationContext方法内调用了collectionView?.dataSource?.collectionView?(moveItemAt:to:)来修改数据源,这本身就不符合UICollectionView的设计规范——布局类只应负责布局相关的无效化处理,数据移动的逻辑应该由UICollectionView的UICollectionViewDataSource代理方法(也就是你已经实现的collectionView(_:moveItemAt:to:))来处理。iOS 16的变更放大了这个不规范写法的风险。
修复方案
1. 移除布局类中修改数据源的代码
把invalidationContext(forInteractivelyMovingItems:)里调用moveItemAt:to:的逻辑删掉,让UICollectionView通过代理方法自动触发数据移动:
open override func invalidationContext(forInteractivelyMovingItems targetIndexPaths: [IndexPath], withTargetPosition targetPosition: CGPoint, previousIndexPaths: [IndexPath], previousPosition: CGPoint) -> UICollectionViewLayoutInvalidationContext { let context = super.invalidationContext(forInteractivelyMovingItems: targetIndexPaths, withTargetPosition: targetPosition, previousIndexPaths: previousIndexPaths, previousPosition: previousPosition) // 移除这里修改数据源的代码,交给UICollectionView的dataSource代理处理 return context }
2. 给强制解包操作加安全校验
如果确实需要在布局方法中处理位置变化,必须先校验数组非空,避免强制解包崩溃:
open override func invalidationContext(forInteractivelyMovingItems targetIndexPaths: [IndexPath], withTargetPosition targetPosition: CGPoint, previousIndexPaths: [IndexPath], previousPosition: CGPoint) -> UICollectionViewLayoutInvalidationContext { let context = super.invalidationContext(forInteractivelyMovingItems: targetIndexPaths, withTargetPosition: targetPosition, previousIndexPaths: previousIndexPaths, previousPosition: previousPosition) // 先校验数组非空,再做后续逻辑 guard let prevIndexPath = previousIndexPaths.first, let targetIndexPath = targetIndexPaths.first, prevIndexPath.item != targetIndexPath.item else { return context } // 这里如果还有布局相关的逻辑,再执行,不要修改数据源 return context }
3. 优化数据源代理方法的逻辑
确保collectionView(_:moveItemAt:to:)只在拖动过程中处理数据,避免不必要的reloadData:
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { // 只有当长按手势未结束时才处理数据移动 guard longPressGesture.state != .ended else { return } let movedItem = availableItems.remove(at: sourceIndexPath.item) availableItems.insert(movedItem, at: destinationIndexPath.item) // 不需要调用reloadData,UICollectionView会自动更新布局 }
内容的提问来源于stack exchange,提问作者smartsanja
相关产品推荐
相关产品推荐

