iOS UICollectionView 的坑:不能重复调用 dequeueReusableSupplementaryViewOfKind

问题描述

今天有个人把他写的 demo 发给我调 bug。他遇到的问题是,在 UICollectionView 的 headerView 上加了一个按钮,但有的 section 的 header 上的按钮点了有反应,有的点了没反应。

我打开 Xcode 的类 reveal 工具一看,点了没反应的那几个 section header 上面都盖着另外一个UICollectionReusableView。按钮被盖住了,难怪点了没反应呢。

section header 被另一个 section header 盖住

但为什么会出现这种情况呢?我们来看他 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,还不容易找到原因。

推荐阅读更多精彩内容