Weihao's Blog

Swift-聊天记录平滑下拉加载(多种类 cell)

当我们在设计聊天界面的时候,由于聊天记录可能存放在服务端,又或者为了考虑内存的因素,我们偏向于在一开始仅仅展示一部分聊天记录,这时候我们就需要有一个方法来实现加载之前的聊天记录。通常是通过下拉来实现的(例如微信)

和下拉刷新的思想很接近,我们可以选择在tableview上加一个refresh control来实现下拉加载,但这里并不推荐使用这个方法(原因后面会提到)但是无论我们使用哪个方法,当我们获取到新的data的时候,需要把他添加到tableView的最上面,这样才符合逻辑。

如图所示 来源:https://bluelemonbits.com/2018/12/30/reloading-inserting-dynamic-height-cells-and-keeping-scroll-position/

由于TableView的特性,在reload的data的时候,会自动跳转到新加入的cell的顶端,也就是上图所示绿色的最顶端,这样就会导致每次加载之后整个tableView都会产生突兀的跳转。

这时候我们需要的解决方法是像参考微信一样,在加入新的Cell的同时,保持我们当时的contentOffset。具体实现这个的代码如下:

let oldContentSizeHeight = ListView.contentSize.height
ListView.reloadData() 
ListView.layoutIfNeeded()
let newContentSizeHeight = ListView.contentSize.height
                ListView.setContentOffset(CGPoint(x:ListView.contentOffset.x, y:newContentSizeHeight - oldContentSizeHeight), animated: false)

简而言之,就是在reload data之前记录此时的content height,待reload结束之后再记录之后的height,通过相减来设置contentOffset

这时候我们需要了解tableView在计算content height的原理,如果只是简单的单一的cell,那么cell的height会是相同的,并且是在

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { }

这个delegate方法里设定的,但是如果我们的cell是多种类的呢,这时候iOS提供了一个estimate height 对于cell

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}

通过同时实现这个delegate和上一个delegate的方法,iOS会自动帮我们计算出合适的content height,这样就不会产生有问题height,从而导致跳转出现问题(换句话说,如果你不实现这个方法,那么计算你content height的方法,就是简单的cell number * [cell height])

通过以上方式,我们可以实现固定位置的上拉刷新,回到之前提到的问题,为什么不推荐使用refresh control,原因其实很简答,因为refresh control是通过加载一个新的uiView在tableView上面,然后设定一个阈值,当我们下拉到一定程度会调用。那对于用户的体验来讲,需要一直下拉刷新的话,并不让人愉悦。通常快速向上滑动的时候并不会触发这个调用,也就会导致我们需要很多次“两次操作”才能到达我们想要的聊天记录的位置。那么我这里用到的替代方法是:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == ListView {
            if ListView.contentOffset.y < 0 {
                if !isLoading {
                    loadPreviousMessage()
                }
            }
        }
    }

因为TableView的实现是在scrollView上的,所以我们可以检测是否scrollView的contentOffset < 0 来检测用户是否想要加载之前的聊天记录,同时因为scrollView的特性是支持bounce的,所以向上快速滑动的同时,contentOffset也会出现 < 0的情况,这里为了避免多次调用,使用了一个变量 “isLoading”来控制。

这样设定之后,我们就可以简单的实现一个向上滑动来加载之前的聊天记录的功能