You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

UICollectionView Footer切后台后无法保持展开问题求助

问题: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:57:27