[译]Swift 中的通用数据源

Swift 中的通用数据源

在我开发的绝大多数 iOS app 中, tableView 和 collectionView 绝对是最经常使用的 UI 组件。鉴于设置一个 tableView 或 collectionView 须要大量样板代码,我最近花了些时间找到一个比较好的方法,去避免一遍又一遍地重复一样的代码。个人主要工做是对必需的样板代码进行抽取封装。随着时间的推移,不少其余开发者也解决了这个问题。而且随着 Swift 的最新进展出现了不少有趣的解决方案。前端

本篇文章里,我将介绍在我 APP 里已经使用了一段时间的解决方案,这个方案让我在设置 collectionView 的时候减小了大量的样板代码。react

TableView vs CollectionView

有些人可能会问 为何单讨论 collectionView 而不提 tableView 呢?android

在最近的几个月里,我在以前可使用 tableView 的地方都使用成了 collectionView 。它们到目前为止表现良好!这一作法帮助我不用去区分这两个 几乎彻底 类似但并不彻底相同的集合概念。接下来则是让我作出这一决定的根本缘由:ios

  • 任何 tableView 均可以用单列的 collectionView 进行实现/重构。
  • tableView 在大屏幕上(如:iPad )表现的不是特别好。

须要说明的是,我没有建议你把代码库里全部的 tableView 都用 collectionView 从新实现。我建议的是,当你须要添加一个展现列表的新功能时,你应该考虑下使用 collectionView 来代替 tableView 。尤为是在你开发一个 Universal APP 时,由于 collectionView 将让你的 APP 在全部尺寸屏幕上动态调整布局变得更简单。git

Swift 泛型与有效抽取的探索

我一直是泛型编程的拥趸,因此你能想象的到当苹果宣布在 Swift 中引进泛型时,我是多么的兴奋。可是泛型和协议结合有时并不合做的那么和谐。这时 Swift 2.x 中关于 关联类型 的介绍让使用泛型协议变得更加简单,愈来愈多的开发者开始去尝试使用它们。github

我打算展现的代码抽取是基于对泛型使用的尝试,尤为是泛型协议。这样的代码抽取可以让我对设置 collectionView 所需的样板代码进行封装,从而减小设置数据源所需的代码,甚至在一些简单的使用场景两行代码就足够了。编程

我想说明下我所建立的不是通解。我作的代码封装针对于解决一些特定使用场景。对于这些场景来讲,使用抽取封装后的代码效果很是好。对于一些复杂的使用场景,可能就须要添加额外的代码了。我把抽取工做主要放在了 collectionView 最经常使用的功能。若是须要的话,你能够封装更多的功能,可是对于个人特定场景来讲,这并非必需的。swift

做为本篇文章的目的,我将会展现一部分抽取代码来归纳使用 collectionView 时经常使用的功能。这将是你了解使用泛型,尤为是泛型协议可以来作什么的一个好的机会。后端

Collection View Cell 抽取

首先,我实现 collectionView 一般都是先建立展现数据的 cell 。处理 collectionView 的 cell 时一般须要:api

  • 重用 cell
  • 配置 cell

为了简化上面的工做,我写了两个协议:

  • ReusableCell
  • ConfigurableCell

让咱们详细地看一下这两个抽取后代码吧。

ReusableCell

这个 ReusableCell 协议须要你定义一个 重用标识符 ,这个标志符将在重用 cell 的时候被用到。在个人 APP 里,我老是图方便把 cell 的重用标识符设置为和 cell 的类名同样。所以,很容易经过建立一个协议扩展来抽取出,让 reuseIdentifier 返回一个带有类名称的字符串:

public protocol ReusableCell {
    static var reuseIdentifier: String { get }
}

public extension ReusableCell {
    static var reuseIdentifier: String {
        return String(describing: self)
    }
}复制代码

ConfigurableCell

这个 ConfigurableCell 协议须要你实现一个方法,这个方法将使用特定类型的实例配置 cell ,而这个实例被定义成了一个泛型类型 T:

public protocol ConfigurableCell: ReusableCell {
    associatedtype T

    func configure(_ item: T, at indexPath: IndexPath)
}复制代码

这个 ConfigurableCell 协议将会在加载 cell 内容的时候被调用。接下来我会详细介绍一些细节,如今我就强调下一些地方:

  1. ConfigurableCell 继承 ReusableCell

  2. 绑定类型的使用( 绑定类型 T )将 ConfigurableCell 定义为泛型协议。

数据源的抽取: CollectionDataProvider

如今,让咱们把目光收回,再回想下设置 collection view 都须要作些什么。为了让 collection view 展现内容,咱们须要遵循 UICollectionViewDataSource 协议。那么最早要作的经常是肯定下来这些:

  • 须要几组:numberOfSections(in:)
  • 每组须要几行:collectionView(_:numberOfItemsInSection:)
  • cell 的内容怎么加载 :collectionView(_:cellForItemAt:)

将上述代理方法实现,会确保咱们可以对指定 collectionView 的 cell 进行展现 。而对于我来讲,这里是很是适合进行代码抽取的地方。

为了抽取和封装上述步骤,我建立了如下泛型协议:

public protocol CollectionDataProvider {
    associatedtype T

    func numberOfSections() -> Int
    func numberOfItems(in section: Int) -> Int
    func item(at indexPath: IndexPath) -> T?

    func updateItem(at indexPath: IndexPath, value: T)
}复制代码

这个协议前三个方法是:

  • numberOfSections()
  • numberOfItems(in:)
  • item(at:)

他们指明了遵循 UICollectionViewDataSource 协议须要实现的代理方法列表。基于我有过一些当用户交互后须要更新数据源的使用场景,我在最后又加了一个 (updateItem(at:, value:)) 方法。这个方法容许你在须要的时候更新底层数据。到这里,在 CollectionDataProvider 定义的方法知足了遵循 UICollectionViewDataSource 协议时须要实现的经常使用功能。

封装样板: CollectionDataSource

经过上面的抽取,如今能够开始实现一个基类,这个基类将被封装为 collectionView 建立数据源所需的经常使用样板。这就是最神奇地方!这个类的主要做用就是利用特定的 CollectionDataProviderUICollectionViewCell 来知足遵循 UICollectionViewDataSource 协议所须要实现的方法。

这是这个类的定义:

open class CollectionDataSource<Provider: CollectionDataProvider, Cell: UICollectionViewCell>:
    NSObject,
    UICollectionViewDataSource,
    UICollectionViewDelegate,
    where Cell: ConfigurableCell, Provider.T == Cell.T
{ [...] }复制代码

它为咱们作了不少事:

  1. 这个类有一个公有属性,让咱们可以将它扩展为指定 CollectionDataProvider 提供正确的实现。
  2. 这是一个泛型的类,因此它须要特定的 Provider (CollectionDataProvider) 和 Cell (UICollectionViewCell) 对象进一步的定义来使用。
  3. 这个类继承于 NSObject 基类,因此可以遵循 UICollectionViewDataSourceUICollectionViewDelegate 来进行抽取封装样板代码。
  4. 这个类在如下场景使用的时候有一些特定限制:
  • UICollectionViewCell 必须遵循 ConfigurableCell 协议。( Cell: ConfigurableCell
  • 特定类型 T 必须和 cell 跟 Provider 的 T 相同 (Provider.T == Cell.T)。

代码须要像下面同样对 CollectionDataSource 进行初始化和设置:

// MARK: - Private Properties
let provider: Provider
let collectionView: UICollectionView

// MARK: - Lifecycle
init(collectionView: UICollectionView, provider: Provider) {
    self.collectionView = collectionView
    self.provider = provider
    super.init()
    setUp()
}

func setUp() {
    collectionView.dataSource = self
    collectionView.delegate = self
}复制代码

代码是很是简单的:CollectionDataSource 须要知道它将针对哪一个 collectionView 对象,将根据哪一个做为数据提供者。这些问题都是经过 init 方法的参数进行传递肯定的。在初始化的过程当中,CollectionDataSource 将本身设置为 UICollectionViewDataSourceUICollectionViewDelegate 的代理对象(在 setUp 方法中)。

如今让咱们看一下 UICollectionViewDataSource 代理的样板代码。

这是代码:

// MARK: - UICollectionViewDataSource
public func numberOfSections(in collectionView: UICollectionView) -> Int {
    return provider.numberOfSections()
}

public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return provider.numberOfItems(in: section)
}

open func collectionView(_ collectionView: UICollectionView,
     cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier,
        for: indexPath) as? Cell else {
        return UICollectionViewCell()
    }
    let item = provider.item(at: indexPath)
    if let item = item {
        cell.configure(item, at: indexPath)
    }
    return cell
}复制代码

上面的代码片断经过 CollectionDataProvider 的一个对象展现了 UICollectionViewDataSource 代理的主要实现,就像以前所说的那样,它封装了数据源实现的全部细节。每一个代理都使用指定的 CollectionDataProvider 方法来抽取跟数据源之间进行交互。

注意 collectionView(_:cellForItemAt:) 方法有一个公开的属性,这就可以让它的任何子类在须要对 cell 内容进行更多定制化的时候进行扩展。

如今对 collectionView cell 展现的功能已经作好了,让咱们再为它添加更多的功能吧。

而做为第一个要添加的功能,用户应该可以在点击 cell 的时候触发某些操做。为了实现这个功能,一个简单的方案就是定义一个简单的 closure,并对这个 closure 初始化,当用户点击 cell 的时候执行这个 closure 。

处理 cell 点击的自定义 closure 以下所示:

public typealias CollectionItemSelectionHandlerType = (IndexPath) -> Void复制代码

如今,咱们能定义个属性来存储这个 closure ,当用户点击这个 cell 的时候就会在 UICollectionViewDelegatecollectionView(_:didSelectItemAt:) 代理方法实现中执行这个初始化好的 closure 。

// MARK: - Delegates
public var collectionItemSelectionHandler: CollectionItemSelectionHandlerType?

// MARK: - UICollectionViewDelegate
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    collectionItemSelectionHandler?(indexPath)
}复制代码

做为第二个要添加的功能,我打算在 CollectionDataSource 中对多组组头和组的一些代码样板进行封装。这就须要实现 UICollectionViewDataSource 的代理方法 viewForSupplementaryElementOfKind 。为了可以让子类自定义的实现 viewForSupplementaryElementOfKind ,这个代理方法须要定义为公开方法,以便让任何子类可以对这个方法进行重写。

open func collectionView(_ collectionView: UICollectionView,
    viewForSupplementaryElementOfKind kind: String,
    at indexPath: IndexPath) -> UICollectionReusableView
{
    return UICollectionReusableView(frame: CGRect.zero)
}复制代码

一般来讲,这种方式适用于全部的代理方法,当他们须要被子类重写覆盖时,这些方法须要定义为公有方法,并在 CollectionDataSource 中实现。

另外一种不一样的解决方案就是使用一个自定义的 closure ,就像在 (CollectionItemSelectionHandlerType) 方法中处理 cell 点击事件同样。

我实现的这个特定方面是软件工程中的一个典型的权衡,一方面 —— 为 collectionView 设置数据源的主要细节都被隐藏(被抽取封装)。另外一方面 —— 封装的样板代码中没有提供的功能,就会变得不能开箱即用,添加新的功能并不复杂,可是须要像我上面两个例子那样,须要实现更多的自定义代码。

实现一个具体的 CollectionDataProvider 也就是 ArrayDataProvider

如今样板代码已经设置好了,collectionView 的数据源由 CollectionDataSource 负责。让咱们经过一个普通的使用案例来看看样板代码用起来有多方便。为了作这个,CollectionDataSource 对象须要提供 CollectionDataProvider 具体的实现。一个覆盖大多数常见使用案例的基本实现,能够简单地使用二维数组来包含展现 collectionView cell 内容的数据 。做为我对数据源抽象的试验的一部分,我使这个实现变得更加通用,而且可以表示:

  • 二维数组,每个数组元素表明 collectionView 一组 cell 的内容。
  • 数组,表示 collectionView 只有一组 cell 的内容(没有组头)。

上面的代码实现都包含在泛型类 ArrayDataProvider 中:

public class ArrayDataProvider<T>: CollectionDataProvider {
    // MARK: - Internal Properties
    var items: [[T]] = []

    // MARK: - Lifecycle
    init(array: [[T]]) {
        items = array
    }

    // MARK: - CollectionDataProvider
    public func numberOfSections() -> Int {
        return items.count
    }

    public func numberOfItems(in section: Int) -> Int {
        guard section >= 0 && section < items.count else {
            return 0
        }
        return items[section].count
    }

    public func item(at indexPath: IndexPath) -> T? {
        guard indexPath.section >= 0 &&
            indexPath.section < items.count &&
            indexPath.row >= 0 &&
            indexPath.row < items[indexPath.section].count else
        {
            return items[indexPath.section][indexPath.row]
        }
        return nil
    }

    public func updateItem(at indexPath: IndexPath, value: T) {
        guard indexPath.section >= 0 &&
            indexPath.section < items.count &&
            indexPath.row >= 0 &&
            indexPath.row < items[indexPath.section].count else
        {
            return
        }
        items[indexPath.section][indexPath.row] = value
    }
}复制代码

这样作能够提取访问数据源的细节,线性数据结构能够表示 cell 的内容是最多见的使用状况。

封装到一块: CollectionArrayDataSource

这样 CollectionDataProvider 协议就具体实现了,建立一个 CollectionDataSource 子类来实现最多见的简单的列表数据展现是很是容易的。

让咱们从这个类的定义开始:

open class CollectionArrayDataSource<T, Cell: UICollectionViewCell>: CollectionDataSource<ArrayDataProvider<T>, Cell>
     where Cell: ConfigurableCell, Cell.T == T
 { [...] }复制代码

这个声明定义了不少事情:

  1. 这个类有一个公有的属性,由于它最终将被扩展为 UICollectionView 对象的数据源对象。
  2. 这是一个继承 UICollectionViewCell 的泛型类,须要被特定的类型 T 进一步定义才能正确展现 cell 和 cell 的内容。

  3. 这个类扩展了 CollectionDataSource 来提供进一步的特定行为。

  4. 特定类型 T 将被表示,它将经过一个 ArrayDataProvider < T > 对象来访问 cell 内容。

  5. 这个类在 closure 中的定义代表有些特定的约束:

  • UICollectionViewCell 必须遵循 ConfigurableCell 协议。( Cell: ConfigurableCell
  • cell 中的特定类型 T 必须跟 Provider 的 T 相同 (Provider.T == Cell.T) 。

类的实现很是简单:

// MARK: - Lifecycle
public convenience init(collectionView: UICollectionView, array: [T]) {
   self.init(collectionView: collectionView, array: [array])
}

public init(collectionView: UICollectionView, array: [[T]]) {
   let provider = ArrayDataProvider(array: array)
   super.init(collectionView: collectionView, provider: provider)
}

// MARK: - Public Methods
public func item(at indexPath: IndexPath) -> T? {
   return provider.item(at: indexPath)
}

public func updateItem(at indexPath: IndexPath, value: T) {
   provider.updateItem(at: indexPath, value: value)
}复制代码

它只是提供了一些初始化方法和与交互方法,这些方法使咱们可以让数据提供者与数据源透明地进行读取和写入操做。

建立一个基本的 CollectionView

能够将 CollectionArrayDataSource 基类扩展,为任何能够用二维数组展现的 collection view 建立一个特定的数据源。

class PhotosDataSource: CollectionArrayDataSource<PhotoViewModel, PhotoCell> {}复制代码

声明比较简单:

  1. 继承于 CollectionArrayDataSource
  2. 这个类表示 PhotoViewModel 做为特定类型 T 将会展现 cell 内容,可经过 ArrayDataProvider < PhotoViewModel > 对象访问,PhotoCell 将做为 UICollectionViewCell 展现。

请注意,PhotoCell 必须遵照 ConfigurableCell 协议,而且可以经过 PhotoViewModel 实例初始化它的属性。

建立一个 PhotosDataSource 对象是很是简单的。只须要传递过去将要展现的 collectionView 和由展现每一个 cell 内容的 PhotoViewModel 元素组成的数组:

let dataSource = PhotosDataSource(collectionView: collectionView, array: viewModels)复制代码

collectionView 参数一般是 storyboard 上的 collectionView 经过 outlet 指向获取到的。

全部的就完成了!两行代码就能够设置一个基本的 collectionView 数据源。

设置带有组标题和组的 CollectionView

对于更高级和复杂的用例,你能够简单在 GitHub repo 上查看 TaskList 。内容已经很长了,本文就再也不不介绍示例的更多细节。我将在下一篇 “Collection View with Headers and Sections” 文章里进行深刻地探讨。在这个说明中,若是存在一个话题对你来讲颇有意思,请不要犹豫让我知道,这样我就能够优先考虑下一步写什么。为了和我联系,请在这篇文章下方留言或发邮件给我: andrea.prearo@gmail.com

结论

在这篇文章中,我介绍了一些我作的抽取封装,以简化使用泛型数据源的 collectionView 。所提出的实现都是基于我在构建 iOS app 时遇到的重复代码的场景。一些更高级的的功能可能须要进一步的自定义。我相信,继续优化所获得的代码抽取,或者构建新的代码抽取,来简化处理不一样的 collectionView 模式都是可能的。但这已经超出了这篇文章的范围。

全部的通用数据源代码和示例工程都在 GitHub 而且是遵照 MIT 协议的。你能够直接使用和修改它们。欢迎全部的反馈意见和建议的贡献,并不是常感谢你这么作。若是你有足够的兴趣,我将很乐意添加所需的配置,使代码与Cocoapods和Carthage一块儿使用,并容许使用这种依赖关系管理工具导入通用数据源。或者,这多是一个很好的起点去为这个项目作出贡献。


额外连接

披露声明:这些意见是做者的意见。 除非在文章中额外声明,不然 Capital One 版权不属于任何所说起的公司,也不属于任何上述公司。 使用或显示的全部商标和其余知识产权均为其各自全部者的全部权。 本文版权为 ©2017 Capital One

更多关于 API、开源、社区活动或开发文化的信息,请访问咱们的一站式开发网站 developer.capitalone.com


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划

相关文章
相关标签/搜索