由一个项目中的问题想到的。
为什么会想到讨论这个问题呢?以前在使用UITableView的时候,也曾有过疑问,那就是同样是在tableView的缓存中获取cell,下面的两个方法究竟有何不同
func dequeueReusableCell(withIdentifier: String, for: IndexPath) -> UITableViewCell
func dequeueReusableCell(withIdentifier identifier: String) -> UITableViewCell?
根据官网有IndexPath以及无IndexPath上的说法,
两者的介绍非常的相似,一个 tableView 通常会维持一个队列的 cell,用以给dataSource复用。调用这个方法时,就是要求 tableView 返回一个新的 cell。如果队列中有现成的 cell,那可以直接拿出来复用。如果没有现成的 cell,可以通过注册的 nib file 或者是 class 重新创建。
如果是通过注册的class进行创建,则需要调用方法 init(style: reuseIdentifier:)
方法进行创建。对于基于 nib 的 class,则是直接通过nib文件进行创建。如果存在可以直接复用的cell,则会调用cell的 prepareForReuse()
方法。
那么看起来,这两个方法仅仅有两点不一样
dequeueReusableCell(withIdentifier: String, for: IndexPath) | dequeueReusableCell(withIdentifier: String) |
---|---|
不能返回nil | 可以返回nil |
有indexPath参数 | 没有indexPath参数 |
需要注意的是,通常会有老一些的说法,说是在有 indexPath 的方法中,如果已经对该 cell 进行了注册,则肯定会返回一个对应类型的cell。 而另外一个没有 indexPath 的方法中,则不会自动创建,需要用户进行手动创建。
在最新的文档中,两个方法的说明都分别说明,只有对于 cell 进行了注册,则无论哪个 dequeue 的方法都会返回一个对应的 cell。
那么到这里,作者就会有一个疑问了,indexPath 起到了一个什么样的作用呢? 我们先来看一下最近作者在项目中遇到的一个问题。
问题
问题代码
问题的背景是一个竖直方向的 UITableView, 其中一种 cell 包裹可以水平滑动的UICollectionView。
啥也不说,上代码。
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: FocusRecommentMediaTableViewCell.cellId, for: indexPath) as? FocusRecommentMediaTableViewCell else {
return FocusRecommentMediaTableViewCell()
}
cell.collectionView.dataSource = self
//还有可能是其他类型的cell
return cell
通过代码我们可以粗略的了解到,当前是一个 tableView 的返回 cell 的方法。这个 UITableViewCell 中又嵌套了一个 UICollectionView。
需要提一点的是,我在对应的 UITableViewCell 的初始化方法中,并没有对其持有 UICollectionView 的 dataSource 进行赋值。即初始化时 collectionView.dataSource == nil
。
意外
项目中通常跑的都是没有问题的,直到测试到某一个手机 iphone6, iOS9.3
此时,在缓慢向下滑动的情况下
,系统是没有问题的。
在用户快速向下滑动,即将出现此类 cell 时,会出现崩溃。
问题描述: UICollectionView dataSource is not set
.
在添加了Exception 捕获后,得到以下提示:
*** Assertion failure in -[CollectionView _createPreparedSupplementaryViewForElementOfKind:atIndexPath:withLayoutAttributes:applyAttributes:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3512.60.7/UICollectionView.m:1599
作者有对更高级的iOS系统进行了测试,发现完全没有这个问题。
那既然提示,DataSource没有进行设置,可能是调用时机的问题,因此,比较粗劣的解决方案是,在UITableView进行初始化,并对UICollectionView进行配置时,设置一个临时的dataSource。
但是这个解决方案仍然不能解决我的疑惑,经过进一步的定位,发现崩溃的具体位置:
guard let cell = tableView.dequeueReusableCell(withIdentifier: FocusRecommentMediaTableViewCell.cellId, for: indexPath) as? FocusRecommentMediaTableViewCell else {
return FocusRecommentMediaTableViewCell()
}
即在调用tableView.dequeueReusableCell(withIdentifier: for:)
方法时发生了崩溃。在这个时候,UITableViewCell还没有返回给cell, UICollectionView还没有设置dataSource。让我好奇的是,这个时机应该是UICollectionView在进行初始化的时候,没有进行reload操作
,为何会调用到dataSource,而在iOS9以后则没有这个问题。而且必须是快速滑动的情况才会发生。
这个时候大家就会问了,上面磨磨唧唧说了半天的为题,跟这次的题目有什么关系?
是的,将上面的方法换成tableView.dequeueReusableCell(withIdentifier:)
,也就是去除掉indexPath这个参数以后,就完全不会出现上面的问题了。
那么关键的问题来了,对于此处非常重要的一个参数 indexPath, 它在此处起了一个什么样的作用?
在developer.apple的文档里, 有一句一笔带过的说明
This method uses the index path to perform additional configuration based on the cell’s position in the table view.
说的是func dequeueReusableCell(withIdentifier identifier: String,
这个方法会使用indexPath来进行一些基于cell位置的额外配置。那这是配置是什么呢?
for indexPath: IndexPath) -> UITableViewCell
我还要看看!
to be continued...