[譯]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前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索