MVVM模式下清空CollectionView的正确方式及崩溃排查
这个问题的核心原因有两个:一是你的ViewModel中numberOfItems(inSection:)方法没有做越界防御判断,二是UICollectionView在reloadData过程中可能存在旧布局状态的残留,导致它尝试请求一个已经不存在的section的item数量。
先看崩溃的直接诱因
你看到错误提示request for number of items in section 6 when there are only 0 sections,说明UICollectionView确实调用了collectionView(_:numberOfItemsInSection:)方法,而且传入的section是6——只是你可能没捕捉到这个调用(比如断点没触发、日志没打印)。此时你的childVMs已经被清空,childVMs[section]直接访问空数组的索引,必然导致崩溃。
解决方案
1. 给ViewModel添加防御性判断(最关键)
修改ViewModel的numberOfItems(inSection:)方法,先判断section是否在合法范围内:
class ViewModel { private var childVMs: [ChildViewModel]() var numberOfSections: Int { return childVMs.count } func numberOfItems(inSection section: Int) -> Int { // 先判断section是否越界,越界直接返回0 guard section >= 0, section < childVMs.count else { return 0 } return childVMs[section].numberOfItems } func clear() { childVMs.removeAll() } ... }
这一步是兜底操作,不管UICollectionView什么时候传入非法section,都不会崩溃。
2. 确保reloadData在主队列执行
UI操作必须在主队列执行,虽然你的logout方法大概率已经在主队列,但为了避免异步回调导致的问题,明确指定主队列:
func logout() { vm.clear() DispatchQueue.main.async { self.collectionView?.reloadData() } }
3. 可选:用批量更新替代直接reloadData
如果想更精准地同步数据源变化,可以用performBatchUpdates来删除所有section,避免旧布局状态干扰:
func logout() { // 先记录当前的section数量 let oldSectionCount = vm.numberOfSections vm.clear() collectionView?.performBatchUpdates({ let sectionsToDelete = IndexSet(integersIn: 0..<oldSectionCount) self.collectionView?.deleteSections(sectionsToDelete) }, completion: nil) }
为什么会出现"numberOfSections返回0但numberOfItemsInSection被调用"的现象?
UICollectionView的reloadData并不是完全原子性的操作,它的布局系统(比如UICollectionViewFlowLayout)可能会在后台异步计算布局,或者保留了部分旧的状态信息。当你清空数据源后调用reloadData,虽然numberOfSections已经返回0,但布局系统可能还在处理之前的布局请求,导致它尝试获取某个旧section的item数量。这时候你的数据源方法如果没有防御判断,就会崩溃。
内容的提问来源于stack exchange,提问作者Jack Guo

