Swift中的通用数据源

在我从事的绝大多数iOS应用程序中,表视图和集合视图是最常用的UI组件。 由于设置表视图或集合视图需要很多样板,因此我最近花了一些时间来寻找一种避免一遍又一遍地编写相同代码的好方法。 我的工作重点是尝试通过一组抽象来封装所需的样板。 随着时间的流逝,许多其他开发人员都致力于解决这个问题,并且随着Swift的最新发展,已经开发了许多有趣的方法。

在这篇文章中,我将说明一段时间以来一直在使用的方法,以减少在应用程序中设置集合视图所需的样板。

表格视图与集合视图

“为什么只谈论集合视图而不是表视图?”你们中的一些人可能会问。

在过去的几个月中,我一直在每个实例中使用过集合视图,而以前,我本可以使用表视图。 到目前为止,它一直运行良好! 它帮助我避免了由于使用两个几乎相似但不完全相同的概念而产生的双重性。 我做出此决定的理由如下:

  • 任何表视图始终可以作为具有一列的集合视图实现/重构。
  • 桌面视图在大屏幕(例如iPad)上无法正常工作。

我想指出的是,我不建议您遍历代码库并将所有表视图重新实现为集合视图。 我的建议是,如果需要添加需要显示项目列表的新功能 ,则应考虑使用集合视图而不是表视图。 尤其是在使用通用应用程序的情况下,集合视图可能会通过动态调整布局来简化所有屏幕尺寸的工作。

Swift泛型和有用的抽象搜索

我一直是泛型编程的狂热者,所以可以想象,当Apple在Swift中引入泛型时,我感到非常兴奋。 但是,一段时间以来,泛型和协议无法很好地协同工作。 然后,随着Swift 2.x中相关类型的引入,创建通用协议变得更加容易,许多开发人员开始尝试使用它们。

我将要呈现的抽象开始是使用泛型(特别是泛型协议)的实验。 这样的抽象使我能够封装设置集合视图所需的样板,并将为集合视图创建数据源所需的代码减少为两行代码(用于简单用例)。

我想指出的是,我建立的并不是万灵丹。 我实现的抽象专注于解决一组特定的用例。 对于这些情况,它们在简化设置集合视图所需的代码方面做得相当不错。 对于某些更复杂的用例,可能需要其他代码。 我主要致力于隐藏与集合视图相关的最常见功能。 如果需要,可以封装更多功能,但是对于我的特定用例而言,并不是必需的。

出于这篇文章的目的,我将提供一些抽象,这些抽象涵盖使用集合视图时通常需要的功能。 这应该是一个很好的起点,以说明您可以使用泛型(尤其是泛型协议)构建的内容。

集合视图单元格抽象

我通常在实现集合视图时的第一步是创建要用于显示所需数据的单元格。 处理集合视图中的单元格时始终需要执行以下操作:

  • 出队
  • 配置单元

为了简化上述任务,我创建了两个协议:

  • 可重用单元
  • 可配置单元

让我们看一下上述抽象的细节。

可重用单元

ReusableCell协议要求您定义一个reuseIdentifier ,在使单元出队时将使用它。 在我的应用中,我通常采用以下约定:单元标识符与单元类名称相同。 因此,很容易通过创建协议扩展来使它抽象化,该协议扩展使复用标识符返回具有类名的字符串:

可配置单元

ConfigurableCell协议要求您实现一种方法,该方法将用于使用特定类型的实例来配置单元,该实例被声明为通用类型T

当需要加载单元格内容时,将使用ConfigurableCell协议。 我将详细介绍它的一些细节。 就目前而言,我只想强调以下几点:

  1. ConfigurableCell扩展了ReusableCell

2.使用关联类型( associatedtype T )将ConfigurableCell定义为通用协议

抽象数据源:CollectionDataProvider

现在,让我们回到设置集合视图所需的时间。 为了使集合视图显示任何内容,我们需要遵循UICollectionViewDataSource协议。 通常所需的第一步与指定有关:

  • 段数: numberOfSections(in 🙂
  • 每节的行数: collectionView(_:numberOfItemsInSection 🙂
  • 如何加载单元格内容: collectionView(_:cellForItemAt 🙂

上面的步骤实现了代表,以确保我们能够显示特定集合视图的单元格。 因此,对我来说,这看起来像是构建抽象的好地方。

为了抽象和封装上述步骤,我创建了以下通用协议:

协议中的前三种方法是:

  • numberOfSections()
  • numberOfItems(in 🙂
  • 项目(于:)

他们映射实现上面列出的UICollectionViewDataSource委托方法所需的内容 。 由于我有一些用例,我还需要基于一些用户交互来更新数据源,因此我最终添加了第四个方法(updateItem(at :, value :)) ,该方法允许您在需要时更新基础数据源。 因此,在CollectionDataProvider中声明的方法足以封装符合UICollectionViewDataSource所需的通用功能。

封装样板:CollectionDataSource

有了上述抽象,就可以开始实现一个基类,该基类将封装为集合视图创建数据源所需的通用样板。 这就是大多数“魔术”将要发生的地方! 此类的主要职责是利用特定的CollectionDataProviderUICollectionViewCell来实现符合UICollectionViewDataSource协议所需的内容 。 它还将通过遵循UICollectionViewDelegate协议来封装一些常见的单元功能。

这是类声明:

这里发生了很多事情:

  1. 该类具有开放访问属性,因为它将进行扩展以提供将与特定CollectionDataProvider一起使用的具体实现。
  2. 这是一个通用类,它需要通过定义将使用的Provider(CollectionDataProvider)和Cell (UICollectionViewCell)的特定实例来进一步规范。
  3. 该类扩展了NSObject并符合UICollectionViewDataSourceUICollectionViewDelegate,以实现和封装样板代码。
  4. 该类在where子句中声明了几个特定的​​约束:
  • 它接受的UICollectionViewCell必须符合ConfigurableCell协议( Cell: ConfigurableCell )。
  • 单元和提供的特定类型T必须相同( Provider.T == Cell.T )。

设置和初始化CollectionDataSource类所需的代码如下:

代码非常简单: CollectionDataSource需要知道它将对哪个集合视图实例以及通过哪个特定Provider进行操作。 这两个元素都作为init方法的参数传递。 在初始化阶段, CollectionDataSource将自己设置为UICollectionViewDataSourceUICollectionViewDelegate的委托(在setUp方法中)。

现在,让我们看一下实现UICollectionViewDataSource的委托的样板代码。

这是代码:

上面的代码片段通过CollectionDataProvider的一个实例显示了主UICollectionViewDataSource委托的实现, 如前所述 ,该实例封装了数据源实现的详细信息。 每个委托都使用特定的CollectionDataProvider方法来抽象与数据源的交互。

请注意, collectionView(_:cellForItemAt 🙂方法具有打开访问属性。 如果任何子类在单元内容初始化阶段需要更多自定义,则可以对其进行扩展。

现在已经可以在集合视图中显示单元格的功能,让我们添加更多功能。

对于第一个附加功能,用户应该能够点击一个单元格并触发一些动作。 为了实现这一点,一个简单的解决方案是定义一个自定义闭包,如果分配了闭包,则在用户点击一个单元格时执行它。

用于处理单元水龙头的自定义闭包如下所示:

现在,我们可以声明一个属性来存储闭包,并实现UICollectionViewDelegatecollectionView(_:didSelectItemAt 🙂方法以在用户点击单元格时执行分配的闭包:

对于第二个附加功能,我将实现一些样板,以处理CollectionDataSource中的多个标题和节。 这需要实现UICollectionViewDataSource的viewForSupplementaryElementOfKind委托方法。 因为我想封装用于在CollectionDataSource中设置委托的所有逻辑,为了使子类能够自定义viewForSupplementaryElementOfKind ,应该使用开放属性访问器声明委托方法,以使其在任何子类中都可重写:

一般来说,对于所有委托方法都是如此。 如果它们需要被子类覆盖,则需要在CollectionDataSource中实现它们,并使用开放属性访问器对其进行声明。

实现相同目标的另一种策略是使用自定义闭包,如单元格轻点处理(CollectionItemSelectionHandlerType)所示

我的实现的这一特殊方面是软件工程中的一个典型折衷。 一方面-设置视图集合的数据源的大多数细节将被隐藏(并抽象掉)。 另一方面-尚未作为样板的一部分提供的所有功能将无法“开箱即用”使用,并且需要进行额外的自定义。 添加新功能并不是太复杂,但是需要实现更多自定义代码,如上述两个示例所示。

实现一个具体的CollectionDataProvider:ArrayDataProvider

现在已经设置好样板,可以通过CollectionDataSource处理用于集合视图的数据源。 让我们看看如何在一个非常常见的用例中利用它。 为此,让我们先回到CollectionDataProvider协议。 为了能够创建CollectionDataSource的实例,需要提供CollectionDataProvider的具体实现。 涵盖大多数常见用例的基本实现可以简单地利用数组类型来表示包含要在集合视图单元格中显示的数据的项目列表。 作为我对数据源抽象的实验的一部分,我使该实现更加通用并且能够表示:

  • 列表数组,其中列表中的每个列表代表集合视图一部分的内容。
  • 一个项目的单个列表,表示集合视图的单元格的数据,表示为仅具有一个部分(没有标题)的等效项。

上述实现的代码包含在通用类ArrayDataProvider中

对于线性数据结构可以表示单元格内容的最常见用例,这需要抽象访问数据源的细节。

将它们全部包装在一起:CollectionArrayDataSource

有了CollectionDataProvider协议的具体实现,就可以轻松创建CollectionDataSource的子类,利用它来覆盖非常普遍的用例,在这种情况下,需要显示简单的项目列表。

让我们从类声明开始:

该声明定义了很多东西:

  1. 该类具有开放访问属性,因为它将被扩展以最终为UICollectionView实例创建数据源的实例。
  2. 这是一个通用类,它需要进一步的规范,即基于将要使用的UICollectionViewCell定义将代表单元格内容和Cell的特定类型T。

3.此类扩展了CollectionDataSource以提供进一步的特定行为。

4.特定类型T ,它将表示将通过ArrayDataProvider 实例访问的单元格内容。

5.该类具有几个特定的​​约束,在where子句中声明:

  • 它接受的UICollectionViewCell必须符合ConfigurableCell协议(Cell: ConfigurableCell )。
  • 对于Cell和ArrayDataProvider (Cell。T == T) ,特定类型T必须相同。

类主体非常简单:

它仅提供了两个初始化程序和方法来与提供程序实例透明地交互,以从/向数据源读写项。

设置基本收藏夹视图

可以扩展CollectionArrayDataSource基类,以为可以用项数组表示的任何集合视图创建特定的数据源。 这是一个示例(取自GitHub repo中的PhotoList示例):

声明相对简单:

  1. 该类扩展CollectionArrayDataSource
  2. 此类将PhotoViewModel指定为将表示单元格内容的特定类型T (可通过ArrayDataProvider 实例访问) ,将PhotoCell指定为将要使用的UICollectionViewCell

请注意, PhotoCell必须符合CollectionDataSource声明指定的ConfigurableCell协议,并且能够从PhotoViewModel实例配置其属性。

创建PhotosDataSource的实例非常简单。 它只需要传递将要使用的收集视图和代表每个单元格内容的PhotoViewModel项目数组:

collectionView参数通常是指向情节提要中的集合视图的出口( @IBOutlet弱var collectionView:UICollectionView! )。

就是这样! 只需两行代码即可为基本集合视图设置数据源。

设置带有标题和节的集合视图

对于更高级和更复杂的用例,您可以查看GitHub repo中提供的TaskList示例。 由于内容已经很长,因此我不会在本文中详细介绍该示例。 在下一篇文章中,我可能会更深入地探讨“带有标题和节的集合视图”主题。 关于这一点,如果您对这样的话题感兴趣,请随时告诉我,以便优先处理接下来要写的内容。 要与我联系,请在此帖子上发表评论或发送电子邮件至: andrea.prearo@gmail.com 。

结论

在这篇文章中,我介绍了一些我为简化使用通用数据源使用集合视图而构建的抽象。 拟议的实现基于适合我在构建iOS应用程序期间遇到的重复模式的用例。 一些更高级的用例可能需要进一步的自定义。 我相信,有可能修改所提出的抽象或构建新的抽象,以简化使用不同集合视图模式的工作。 但这不在此特定文章的范围内。

通用数据源和示例应用程序的所有代码都可以在GitHub上的 MIT许可下获得,并且可以自由重用和改编。 欢迎并提供所有反馈以及建议的文稿。 如果有足够的兴趣,我很乐意添加所需的配置,以使代码可与Cocoapods和Carthage一起使用,并允许使用此类依赖项管理工具导入通用数据源。 或者,这可能是为该项目做出贡献的良好起点。