UITableView和UICollectionView中的平滑滚动

正如大多数iOS开发人员所知,显示数据集是构建移动应用程序中的一项相当常见的任务。 Apple的SDK提供了两个组件来帮助执行这样的任务,而不必从头开始执行所有操作:表视图( UITableView 和集合视图( UICollectionView

表格视图和集合视图均旨在支持显示可滚动显示的数据集。 但是,当显示大量数据时,实现完美平滑的滚动可能非常棘手。 这不是理想的,因为它会对用户体验产生负面影响。

作为Capital One Mobile应用程序iOS开发团队的成员,我有机会对表格视图和集合视图进行了尝试。 这篇文章反映了我在显示大量可滚动数据方面的个人经验。 在其中,我们将介绍最重要的技巧,以优化上述SDK组件的性能。 此步骤对于获得非常流畅的滚动体验至关重要。 请注意,以下几点大多数都适用于UITableViewUICollectionView,因为它们共享大量的“ 幕后 ”行为。 关于UICollectionView的几点要点, 因为此视图将其他布局细节放在开发人员的肩膀上。

让我们从快速概述上述组件开始。
UITableView经过优化,可以将视图显示为一系列行。 由于布局是预定义的,因此SDK组件会处理大部分布局,并提供主要专注于显示单元内容的委托。
另一方面, UICollectionView提供了最大的灵活性,因为布局是完全可定制的。 但是,集合视图的灵活性是以必须注意有关如何执行布局的其他细节为代价的。

UITableView和UICollectionView的 共同技巧

注意: 我将对 代码段 使用 UITableView 但是相同的概念也适用于 UICollectionView

单元格渲染是一项关键任务

UITableViewUITableViewCell之间的主要交互可以通过以下事件来描述:

  • 表格视图正在请求需要显示的单元格(tableView(_:cellForRowAt :))。
  • 表格视图即将显示单元格(tableView(_:willDisplay:forRowAt :))。
  • 该单元格已从表视图(tableView(_:didEndDisplaying:forRowAt :))中删除。

对于上述所有事件,表视图都将传递与其进行交互的索引(行)。 这是UITableViewCell生命周期的可视化:

首先,tableView(_:cellForRowAt :)方法应尽可能快。 每次需要显示单元格时都会调用此方法。 它执行得越快,表格视图的滚动就越平滑。

为了确保我们尽可能快地渲染单元,我们可以做一些事情。 以下是来自Apple文档的呈现单元格的基本代码:

在获取了将要重用的单元实例之后(dequeueReusableCell(withIdentifier:for :)),我们需要通过为其属性分配所需的值来对其进行配置。 让我们看一下如何使我们的代码快速执行。

定义单元格的视图模型

一种方法是使我们需要显示的所有属性都易于使用,并将其分配给适当的单元格对应对象。 为了实现这一点,我们可以利用MVVM模式。 假设我们需要在表格视图中显示一组用户 。 我们可以为用户定义模型:

用户定义视图模型很简单:

异步获取数据和缓存视图模型

现在我们已经定义了模型和视图模型,让它们开始工作! 我们将通过Web服务为用户获取数据。 当然,我们希望实现最佳的用户体验。 因此,我们将注意以下事项:

  • 避免在获取数据时阻塞主线程。
  • 检索数据后立即更新表视图。

这意味着我们将异步获取数据。 我们将通过特定的控制器执行此任务,以使获取逻辑与模型和视图模型分离,如下所示:

现在,我们可以检索数据并异步更新表视图,如以下代码片段所示:

我们可以使用以上代码段以几种不同的方式来获取用户数据:

  • 仅当第一次加载表视图时,将其放置在 viewDidLoad()中
  • 每次显示表视图时,将其放置在 viewWillAppear(_ :)中
  • 根据用户需求(例如,通过下拉菜单进行刷新),可以将其放置在将负责刷新数据的方法调用中。

选择取决于后端上数据更改的频率。 如果数据大部分是静态的或不经常更改,则第一种选择更好。 否则,我们应该选择第二个。

异步加载图像并缓存它们

必须为我们的单元加载图像是很常见的。 由于我们试图获得最佳的滚动性能,因此我们绝对不希望阻塞主线程来获取图像。 一种避免这种情况的简单方法是通过围绕URLSession创建一个简单的包装来异步加载图像:

这使我们可以使用后台线程获取每个图像,然后在所需数据可用时更新UI。 通过缓存图像,我们可以进一步提高性能。

如果我们不想(或负担不起)编写自定义异步映像下载并自己缓存,我们可以利用SDWebImage或AlamofireImage之类的库。 这些库提供了我们正在寻找的即用型功能。

自定义单元

为了充分利用缓存的视图模型,我们可以通过将其子类化来定制User单元(从UITableViewCell用于表视图,从UICollectionViewCell用于集合视图)。 基本方法是为需要显示的Model的每个属性创建一个出口,并从View Model对其进行初始化:

使用不透明的图层并避免渐变

由于使用透明层或应用渐变需要大量计算,因此,如果可能,我们应避免使用它们来提高滚动性能。 特别是,我们应该避免更改alpha值,并且最好对单元格及其包含的任何图像使用标准RGB颜色(避免UIColor.clear ):

整合所有内容:优化的单元格渲染

在这一点上,一旦要渲染它,就配置好单元格应该很容易,而且真的很快,因为:

  • 我们正在使用缓存的视图模型数据。
  • 我们正在异步获取图像。

这是更新的代码:

特定于UITableView的技巧

将自调整大小的单元格用于可变高度的单元格

如果我们要在表格视图中显示的单元格具有可变的高度,则可以使用可调整大小的单元格。 基本上,我们应该创建适当的“自动布局”约束,以确保具有可变高度的UI组件可以正确拉伸。 然后,我们只需要初始化EstimatedRowHeight和rowHeight属性:

注意: 在不幸的情况下,我们不能使用自动调整大小的单元格(例如,如果仍然需要支持iOS7),我们必须实现 tableView(_:heightForRowAt 🙂 来计算每个单元格的高度。 但是,仍然可以通过以下方法来提高滚动性能:

  • 一次预先计算所有行高。
  • 调用 tableView(_:heightForRowAt 🙂 时返回缓存的值

特定于UICollectionView的技巧

通过实现适当的UICollectionViewFlowLayoutDelegate协议方法,我们可以轻松地自定义大多数集合视图。

计算你的细胞大小

我们可以通过实现collectionView(_:layout:sizeForItemAt :)自定义集合视图单元格的大小:

手柄尺寸等级和方向变化

在以下情况下,我们应确保正确刷新集合视图的布局:

  • 过渡到另一个尺寸等级。
  • 旋转设备。

这可以通过实现viewWillTransition(to:with :)来实现:

动态调整单元格布局

如果需要动态调整单元格布局,则应通过在自定义集合视图单元格( UICollectionViewCell的子类)中覆盖apply(_ :)来解决此问题

例如,通常在此方法内执行的一项常见任务是通过以编程方式设置多行UILabel的preferredMaxLayoutWidth属性来调整其最大宽度:

结论

您可以在此处找到一个小示例,其中包含针对UITableViewUICollectionView的建议技巧。

在这篇文章中,我们研究了一些实现UITableViewUICollectionView平滑滚动的常用技巧。 我们还介绍了适用于每种特定集合类型的一些特定技巧。 根据特定的UI要求,可能会有更好的方法或不同的方法来优化您的集合类型。 但是,本文中描述的基本原理仍然适用。 而且,像往常一样,找出哪种优化效果最好的最佳方法是分析您的应用程序。