tvOS 15.3+调用UICollectionView.reload()焦点不同步滚动异常
问题表现
同一ViewController内垂直排布两个UICollectionView:
- CollectionViewA:展示序号、图片、收藏/取消收藏操作项
- CollectionViewB:展示节目信息
点击收藏按钮调用两个集合视图的reload()方法刷新收藏状态时,tvOS 15.2及以下版本运行正常;tvOS 15.3及以上版本点击第10行等非顶部行的收藏按钮触发刷新后,集合视图会自动从当前位置滚动偏移到第3或第5行位置,控制台输出如下错误:
Collection view focus state got out of sync. Expected (null) to be the current managed subview but found <MyApp.CollectionViewCell: 0x114517ad0; baseClass = UICollectionViewCell; frame = (0 6532; 270 126); opaque = NO; tintColor = UIExtendedGrayColorSpace 0 0; layer = <CALayer: 0x28339ab00>>
根因说明
tvOS 15.3版本起苹果升级了UICollectionView焦点引擎的内部校验逻辑,当同层级存在多个垂直布局、同步刷新的UICollectionView时,全局调用reloadData()会触发全量cell回收重建,此时系统预期当前获焦cell已经被回收(焦点管理引用为null),但实际cell实例还未完全从焦点链移除,状态校验失败后焦点引擎会自动触发滚动,回滚到系统判定的最近有效可聚焦位置,最终出现自动跳行的异常。低版本系统无该校验逻辑,因此不会触发问题。
可行解决方案
- 方案1:替换全局刷新为定向单元格刷新(优先推荐,改造成本最低)
不要调用两个CollectionView的无差别全量刷新,仅针对收藏状态变更的对应行做局部刷新,从根源避免全量重建导致的焦点链断裂:// 移除原有 collectionViewA.reloadData()、collectionViewB.reloadData() 调用 let targetIndexPath = IndexPath(row: 当前点击的行号, section: 0) // 仅刷新可见范围内的目标单元格,不可见的cell会在复用出列时自动读取最新状态 if collectionViewA.indexPathsForVisibleItems.contains(targetIndexPath) { collectionViewA.reloadItems(at: [targetIndexPath]) } if collectionViewB.indexPathsForVisibleItems.contains(targetIndexPath) { collectionViewB.reloadItems(at: [targetIndexPath]) } - 方案2:刷新前暂存焦点,刷新完成后主动恢复
如果业务场景必须使用全量刷新(比如存在批量状态联动更新),可以在刷新前手动记录当前获焦的位置,等刷新完成后主动把焦点设置回原位置,跳过系统的自动焦点判定逻辑:// 刷新前记录当前获焦的indexPath和所属collectionView var focusedPath: IndexPath? var isFocusInA = false if let focusedCell = UIScreen.main.focusedView as? UICollectionViewCell { if let pathA = collectionViewA.indexPath(for: focusedCell) { focusedPath = pathA isFocusInA = true } else if let pathB = collectionViewB.indexPath(for: focusedCell) { focusedPath = pathB isFocusInA = false } } // 执行全量刷新 collectionViewA.reloadData() collectionViewB.reloadData() // 放到主线程下一轮runloop执行,避开系统刷新的时序冲突 DispatchQueue.main.async { guard let targetPath = focusedPath else { return } let targetView = isFocusInA ? collectionViewA : collectionViewB targetView?.scrollToItem(at: targetPath, at: .centeredVertically, animated: false) targetView?.cellForItem(at: targetPath)?.setNeedsFocusUpdate() targetView?.updateFocusIfNeeded() } - 方案3:调整焦点配置适配高版本系统
如果前两个方案改造成本较高,可以针对tvOS 15.3+系统调整两个CollectionView的焦点行为配置,关闭系统默认的自动焦点滚动逻辑:if #available(tvOS 15.3, *) { collectionViewA.focusGroupBehavior = .none collectionViewB.focusGroupBehavior = .none collectionViewA.remembersLastFocusedIndexPath = false collectionViewB.remembersLastFocusedIndexPath = false }
注意:所有和焦点、contentOffset相关的操作,不要和reloadData()在同一runloop时序执行,必须放到DispatchQueue.main.async块中,否则会加重焦点状态不同步的问题。
内容的提问来源于stack exchange,提问作者Chandru

