使用rx.items时viewForSupplementaryElementOfKind不触发的解决办法
问题
用RxSwift的rx.items绑定UICollectionView时,单元格显示正常,但Header相关代理出现异常:viewForSupplementaryElementOfKind完全不被调用,而referenceSizeForHeaderInSection被多次调用;改用原生UICollectionViewDataSource时Header能正常显示,疑惑是不是因为没正确使用数据源导致的问题。
原因分析
rx.items是RxSwift提供的简化版数据源绑定,它默认只实现了cell相关的数据源方法(比如numberOfItemsInSection、cellForItemAt),没有处理supplementary view(Header/Footer)的数据源逻辑。
虽然你设置了UICollectionViewDelegateFlowLayout的referenceSizeForHeaderInSection返回了Header尺寸,但CollectionView的数据源没有告诉它“当前section需要显示Header”,所以系统不会触发viewForSupplementaryElementOfKind方法,只会反复计算Header尺寸但不渲染。
解决办法
方法1:自定义Rx数据源,实现supplementary view逻辑
放弃rx.items的简化绑定,自己实现RxCollectionViewDataSourceType协议,补充supplementary view的数据源逻辑:
// 自定义数据源类 class ProfileCollectionViewDataSource: NSObject, UICollectionViewDataSource, RxCollectionViewDataSourceType { typealias Element = [Tweet] // 假设你的数据模型是Tweet private var tweets: [Tweet] = [] func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return tweets.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! TweetCell let tweet = tweets[indexPath.item] let tweetCellModel = TweetCellModel(tweet: tweet) cell.bind(cellModel: tweetCellModel) return cell } // 关键:实现supplementary view的数据源方法 func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { if kind == UICollectionView.elementKindSectionHeader { let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerIdentifier, for: indexPath) as! ProfileHeaderView header.viewModel = self.viewModel header.bindViewModel() return header } fatalError("不支持的supplementary类型") } // Rx数据源的更新方法 func collectionView(_ collectionView: UICollectionView, observedEvent: Event<Element>) { Binder(self) { dataSource, items in dataSource.tweets = items collectionView.reloadData() }.on(observedEvent) } } // 在bindViewModel里替换原rx.items绑定 func bindViewModel() { // ... 其他原有代码不变 let dataSource = ProfileCollectionViewDataSource() viewModel.output.userTweets .bind(to: collectionView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) }
方法2:使用RxDataSources库(更推荐)
RxDataSources专门用来处理带section、Header/Footer的列表绑定,配置更简洁:
- 先定义Section模型:
struct ProfileSection { var items: [Item] // 可添加Header需要的额外数据属性 } extension ProfileSection: SectionModelType { typealias Item = Tweet init(original: ProfileSection, items: [Item]) { self = original self.items = items } }
- 配置数据源并绑定:
func bindViewModel() { // ... 其他原有代码不变 let dataSource = RxCollectionViewSectionedReloadDataSource<ProfileSection>( configureCell: { dataSource, collectionView, indexPath, tweet in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! TweetCell let tweetCellModel = TweetCellModel(tweet: tweet) cell.bind(cellModel: tweetCellModel) return cell }, configureSupplementaryView: { dataSource, collectionView, kind, indexPath in if kind == UICollectionView.elementKindSectionHeader { let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerIdentifier, for: indexPath) as! ProfileHeaderView header.viewModel = self.viewModel header.bindViewModel() return header } fatalError("不支持的supplementary类型") } ) // 将数据包装成Section模型后绑定 viewModel.output.userTweets .map { [ProfileSection(items: $0)] } .bind(to: collectionView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) }
内容的提问来源于stack exchange,提问作者kyle

