iOS UICollectionView 的坑:不能重复调用 d
问题描述
今天有个人把他写的 demo 发给我调 bug。他遇到的问题是,在 UICollectionView 的 headerView 上加了一个按钮,但有的 section 的 header 上的按钮点了有反应,有的点了没反应。
我打开 Xcode 的类 reveal 工具一看,点了没反应的那几个 section header 上面都盖着另外一个UICollectionReusableView
。按钮被盖住了,难怪点了没反应呢。

但为什么会出现这种情况呢?我们来看他 header 的注册和回调方法是怎么写的:
header 的注册
[_collectionView registerClass:[Type1HeaderView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:Type1HeaderID];
[_collectionView registerClass:[Type2HeaderView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:Type2HeaderID];
分别用两个 reuseId 注册了两种 header(为了简化,我把命名改了改)。两种 header 所属的类都是UICollectionReusableView
的子类,没什么问题。
header 的回调方法
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
Type1HeaderView *type1HeaderView = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:Type1HeaderID forIndexPath:indexPath];
Type2HeaderView *type2HeaderView = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:Type2HeaderID forIndexPath:indexPath];
if (indexPath.section == 0) {
return type1HeaderView;
} else {
return type2HeaderView;
}
}
return nil;
}
简化之后的代码如上,省去了一些无关细节。代码写得有点随意,但看着还挺正常的。所以我找了半天才找到问题所在。
解决办法
问题就在于那两句
Type1HeaderView* type1HeaderView = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:Type1HeaderID forIndexPath:indexPath];
Type2HeaderView* type2HeaderView = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:Type2HeaderID forIndexPath:indexPath];
连续调用了两次 dequeueReusableSupplementaryViewOfKind: withReuseIdentifier: forIndexPath:
方法,传的是相同的indexPath
。这就导致同一位置上出现了两个重叠的 headerView ,一个盖住另外一个。改成这样:
改正后的回调方法
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
if (indexPath.section == 0) {
Type1HeaderView *type1HeaderView = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:Type1HeaderID forIndexPath:indexPath];
return type1HeaderView;
} else {
Type2HeaderView *type2HeaderView = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:Type2HeaderID forIndexPath:indexPath];
return type2HeaderView;
}
}
return nil;
}
就一点问题都没有了。
大概是每调一次那个dequeue
方法,系统都会在那个indexPath
的位置上创建一个 header,不管最后 return 什么。我觉得这个问题还挺奇怪的,因为查了一下苹果官方文档里并没有提到这一点,可以算是官方的实现产生的一个 bug,也是 UICollectionView 的一个坑了。
结论
在- (UICollectionReusableView *)collectionView: viewForSupplementaryElementOfKind: atIndexPath:
这个回调方法的每次执行中,要么返回 nil,要么调用一次且仅一次dequeueReusableSupplementaryViewOfKind: withReuseIdentifier: forIndexPath:
,千万不能重复调用,否则会导致诡异的 bug,还不容易找到原因。