论 UITableView 的 dequeueReusableCell 方法

2018/8/23 posted in  iOS

由一个项目中的问题想到的。

为什么会想到讨论这个问题呢?以前在使用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,
for indexPath: IndexPath) -> UITableViewCell
这个方法会使用indexPath来进行一些基于cell位置的额外配置。那这是配置是什么呢?
我还要看看!
to be continued...