UICollectionView Footer切后台后无法保持展开问题求助
我通过点击UICollectionViewCell内的按钮来控制footerView的显示和隐藏——在collectionView(_:layout:referenceSizeForFooterInSection)方法里,返回对应高度就显示footer,返回0就隐藏。判断显示状态时,我会在这个方法里获取对应的UICollectionViewCell做校验,但遇到了一个棘手的问题:
- 展开footerView
- 点击Home键切到后台
- 从最近应用列表返回App
- footerView不再保持展开状态
调试后发现,切后台返回后,调用collectionView.cellForItem(at:)获取Cell时返回了nil(查了下原因是Cell不可见时系统会回收,导致无法获取实例)。我试过在viewWillAppear里延迟1秒调用self.collectionView?.collectionViewLayout.invalidateLayout()来重新加载footer,但没解决问题。
相关代码
override func viewWillAppear(_ animated: Bool) { var isAnyExpanded = false //Checking if any I need to display footerView of not ... if isAnyExpanded { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self.collectionView?.collectionViewLayout.invalidateLayout() } } } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { ... //other conditions else if messages.count > 0 { let message = messages[0] if message.message_type == .hotel { var cellHeight:CGFloat = 0 if let hotels = arrHotel[message.msg] { let outerIndexPath = IndexPath(item: 0, section: section) if let hotelCell = collectionView.cellForItem(at: outerIndexPath) as? HotelParentCollectionViewCell { //This is where getting nil after coming back from recent app let visibleIndexPaths = hotelCell.subCollectionView.indexPathsForVisibleItems if visibleIndexPaths.count > 0 { let firstIndexPath = visibleIndexPaths[0] let hotel = hotels[firstIndexPath.item] if hotel.isExpanded { //Calculation for height } } } } return CGSize(width: 0, height: cellHeight) } } ... //other conditions return CGSize(width: 0, height: 0) } @objc func btnHotelDetailSelect(sender: UIButton) { var section = 0 if sender.tag > 1000 { section = sender.tag / 1000 } let item = sender.tag - (section * 1000) let message = arrMessageTmp[section][item] if let hotels = arrHotel[message.msg] { let hotel = hotels[item] if hotel.isExpanded { hotel.isExpanded = false } else { for hotels in arrHotel.values { let expandedHotels = hotels.filter({$0.isExpanded}) for hotel in expandedHotels { hotel.isExpanded = false } } hotel.isExpanded = true } collectionView?.collectionViewLayout.invalidateLayout() } }
我来帮你捋捋问题出在哪,以及怎么解决:
核心问题分析
你现在的逻辑是把footer的显示状态依赖于可见的Cell实例,但App切后台再回来时,系统可能会回收不可见的Cell,或者Cell还没完成复用/渲染,这时候cellForItem(at:)就会返回nil,导致你无法正确获取isExpanded状态,最终footer的高度计算错误,被隐藏了。
解决思路
把展开状态的记录从Cell里彻底移出来,存在数据源模型中,这样不管Cell是否可见,都能直接从数据源获取状态,完全不依赖Cell实例。
具体修改步骤
1. 重构referenceSizeForFooterInSection方法
直接从数据源arrHotel中获取isExpanded状态,不要再通过Cell去拿:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { // ...其他条件判断... else if messages.count > 0 { let message = messages[0] if message.message_type == .hotel { var cellHeight: CGFloat = 0 if let hotels = arrHotel[message.msg] { // 直接从数据源找展开的Hotel,不用依赖Cell if let expandedHotel = hotels.first(where: { $0.isExpanded }) { // 这里放你的footer高度计算逻辑 cellHeight = // 比如根据expandedHotel的内容计算高度 } } // 注意width要设为collectionView的宽度,不然footer可能显示不全 return CGSize(width: collectionView.bounds.width, height: cellHeight) } } // ...其他条件... return CGSize(width: 0, height: 0) }
2. 优化viewWillAppear逻辑
去掉不必要的延迟,直接在主线程刷新布局,确保返回App时footer状态正确:
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 别忘了调用super方法 // 检查是否有展开的Hotel let isAnyExpanded = arrHotel.values.flatMap { $0 }.contains { $0.isExpanded } if isAnyExpanded { DispatchQueue.main.async { // 刷新布局,让footer重新计算高度 self.collectionView?.collectionViewLayout.invalidateLayout() // 或者更精准的刷新对应section: // self.collectionView?.reloadSections(IndexSet(integersIn: 0..<self.collectionView.numberOfSections)) } } }
3. 优化按钮点击的状态更新逻辑
你的按钮逻辑已经在更新数据源的isExpanded了,这里可以优化下写法,让代码更简洁:
@objc func btnHotelDetailSelect(sender: UIButton) { let section = sender.tag / 1000 let item = sender.tag - (section * 1000) // 加个越界判断,避免崩溃 guard section < arrMessageTmp.count, item < arrMessageTmp[section].count, let message = arrMessageTmp[section][item], var hotels = arrHotel[message.msg], item < hotels.count else { return } // 先把所有Hotel设为未展开 hotels.forEach { $0.isExpanded = false } // 再把当前点击的Hotel设为展开 hotels[item].isExpanded = true // 更新数据源 arrHotel[message.msg] = hotels // 刷新布局,让footer状态生效 collectionView?.collectionViewLayout.invalidateLayout() }
额外注意事项
- 永远不要依赖
cellForItem(at:)来获取业务状态,这个方法只应该用来操作可见Cell的UI,数据源状态必须独立存储。 - 如果App被系统彻底回收(后台内存不足),内存中的
arrHotel数据会丢失,这时候需要把展开状态持久化,比如存在UserDefaults或者本地数据库里。
内容的提问来源于stack exchange,提问作者Mrugesh Tank

