【iOS】UICollectionView遇到的坑及解决方法(二例记录)

前言

很久没有写文章了,一个是因为五月份一直在学校忙(WAN)毕(YOU)业(XI)的事情,另一个则是因为没有什么好东西值得记录。

六月份回归工作马上做了一个很小的版本,三个组件的排列问题(涉及到autoLayout)算是给我出了个小难题,不过解决之后感觉并不是很有记录的价值,因此文章又少了一篇。

这次的文章算是新鲜出炉的问题记录,来源于我一个正在进行的,一个人负责所有客户端开发的版本(2019.6.20 V3.1.0 进度一半 尚未联调)。


记录

相比于UITableView的使用,在手头的项目里面collection的使用几乎为零,唯一的一次还是我上个版本在UITableViewCell里面使用了UICollectionView(具体为啥我也忘了)。

(1)UICollectionViewCell的视图重叠问题

其实就是发生在上个版本,但这个版本我在使用Xcode的Debug View Hierarchy查看视图层次时才发现了这个问题。

主要表现如下图:

实际表现正常(因为只有一张图,所以重叠也看不出来)

【iOS】UICollectionView遇到的坑及解决方法(二例记录)_第1张图片

实际重叠严重(可能影响性能?)

简单来说就是给同一个cell不断加进了子视图,如果cell变的复杂一点,比如有不同文字,叠加问题就会更加严重了。

首先看一下生成UICollectionViewCell的方法:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    MyCollection *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCellIdentifier forIndexPath:indexPath];
    cell.imageUrl = [self.dataSrc objectAtIndex:indexPath.item];
    return cell;
}

对比UITableViewCell的生成方法:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
    if (nil == cell) {
        //初始化
    }
    cell.record = [self.dataSource objectAtIndex:indexPath.row];
    return cell;
}

和tableView有所区别,UICollectionViewCell必须使用dequeueReusableCellWithReuseIdentifier:forIndexPath创建,这也意味着它没有了判断复用cell是否为nil的必要性。

很容易犯的一个错误就是直接使用UICollectionViewCell,判断dequeueReusableCellWithReuseIdentifier生成的cell是否为空,如果为空就在判断内进行初始化,并且addSubView,然后在判断外赋予数据。

这有两个问题:

1.永远都不会进到nil == cell的判断之中,这就意味着所有初始化操作都是无用功;

2.UICollectionViewCell不应该主动初始化。经过debug可以发现,dequeueReusableCellWithReuseIdentifier其实调用了cell的initWithFrame方法而非init方法。这就说明如果我们想初始化UICollectionViewCell,就应该继承UICollectionViewCell并重写initWithFrame

回到视图重叠的问题,其实产生问题的原因很简单,如果不继承UICollectionViewCell,想对其进行初始化就会写出这样的代码:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCellIdentifier forIndexPath:indexPath];
    const NSInteger imageViewTag = 1002;
    UIImageView *imageView = [cell.contentView viewWithTag:imageViewTag];
    if (nil == imageView) {
        imageView = [[UIImageView alloc] initWithFrame:cell.contentView.bounds];
        imageView.clipsToBounds = YES;
        cell.tag = imageViewTag;
        imageView.layer.cornerRadius = 10;
        imageView.contentMode = UIViewContentModeScaleAspectFill;
        [cell.contentView addSubview:imageView];
    }
    [imageView doSth];
    return cell;
}

初一看好像没有问题,但实际运行出来确实nil == imageView不能如预期执行,猜测是dequeueReusableCellWithReuseIdentifier方法生成的UICollectionViewCell并不是预期中的cell。

综上所述,UICollectionViewCell的正确用法应该是

1.使用dequeueReusableCellWithReuseIdentifier生成UICollectionViewCell,同时赋予数据

2.继承UICollectionViewCell,在initWithFrame中进行初始化

(2)UICollectionViewCell点击事件

在所有点击事件不响应的情况中,最难发现的应该就是点击与手势冲突吧,这里讲的情况也不仅限于UICollectionViewCell。

情况是这样的:

UICollectionViewCell的点击方法实现在其代理UIView上

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    [collectionView deselectItemAtIndexPath:indexPath animated:YES];
}

没什么问题,但是调试时会发现进不到这个方法里面

看代理UIView,没有定义手势;再到上一层的view controller(下面简称VC),在viewDidLoad定义了一个手势

UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapBackground:)];
    tap.delegate = self;
    [self.view addGestureRecognizer:tap];

如果不做其他处理,整个VC及其子View应该都会被屏蔽点击事件,因此VC实现了UIGestureRecognizerDelegate里面的一个方法:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    BOOL ret = NO == ([touch.view isKindOfClass:[HasCellUIView class]]);
    return ret;
}

这个方法好理解,判断touch.view是不是包含了点击事件的UIView,如果是的话就返回NO,告诉VC不处理这次手势并且交给UIView,否则就截断手势,子View将不再会收到点击事件。

最有趣的一点就在于touch.view的规则上了,问题也就变成了touch.view找的究竟是哪个View

正常的想法是直接判断[UICollectionViewCell class],行不通。

因为其实所有add到UICollectionViewCell的View都是add到一个UIView上的,而touch.view找的应该是当前点击控件的最近View。这就导致除非判断[UIView class]否则不可能找到需要处理的touch,但这显然不靠谱(到处都是UIView).

什么叫最近View呢?

UIView *viewA = [[UIView alloc]init];

UIView *viewB = [[UIView alloc]init];

[viewA addSubView:viewB];

UILabel *labelC = [[UILabel alloc]init];

[viewB addSubView:labelC];

如果点击了labelC,那么touch.view == viewB

如果点击了viewB,那么touch.view == viewB

如果点击了viewA,那么touch.view == viewA

解决方案:

为了避免[UIView class]的判断,声明一个继承UIView的背景View,把它加进UICollectionViewCell并且大小相等,然后所有子组件(非UIView)都加进这个背景View里面。这样整个cell的点击事件就可以通过[背景View class]判断出来了。


本次记录稍微零散和随意了一些,有些内容也没有确实验证过,还希望多有指正。

 

 

 

你可能感兴趣的:(【iOS】UICollectionView遇到的坑及解决方法(二例记录))