很久没有写文章了,一个是因为五月份一直在学校忙(WAN)毕(YOU)业(XI)的事情,另一个则是因为没有什么好东西值得记录。
六月份回归工作马上做了一个很小的版本,三个组件的排列问题(涉及到autoLayout)算是给我出了个小难题,不过解决之后感觉并不是很有记录的价值,因此文章又少了一篇。
这次的文章算是新鲜出炉的问题记录,来源于我一个正在进行的,一个人负责所有客户端开发的版本(2019.6.20 V3.1.0 进度一半 尚未联调)。
相比于UITableView的使用,在手头的项目里面collection的使用几乎为零,唯一的一次还是我上个版本在UITableViewCell里面使用了UICollectionView(具体为啥我也忘了)。
(1)UICollectionViewCell的视图重叠问题
其实就是发生在上个版本,但这个版本我在使用Xcode的Debug View Hierarchy查看视图层次时才发现了这个问题。
主要表现如下图:
实际表现正常(因为只有一张图,所以重叠也看不出来)
实际重叠严重(可能影响性能?)
简单来说就是给同一个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]判断出来了。
本次记录稍微零散和随意了一些,有些内容也没有确实验证过,还希望多有指正。