更加 Swift 化的 Collection View 和 Table View Cells

Candra57X 8年前發布 | 13K 次閱讀 Swift Apple Swift開發

來自: http://swift.gg/2016/02/02/being-swifty-with-collection-view-and-table-view-cells/

作者:Jameson Quave, 原文鏈接 ,原文日期:2015-12-28

譯者: CMB ;校對: Cee ;定稿:千葉知風

</div>

</div>

這是一個常見的場景:你有一個 tableView 或者一個 collectionView,并且里面含有大量不同種類的內容。你想做到基于不同種類的內容而展示不一樣的 cell ,而且這些 cell 都混合在同一個部件里(原諒我站在藝術的角度去設計),它看起來就如下圖所示:

在 Objective-C 中,最典型就是使用 NSArray 來記錄 collectionView 的數據源,然后通過對比每個數據源的類型后再對 cell 進行操作,現在看來這種方式是特別不方便的。

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {

    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"identifier" forIndexPath:indexPath];

    id record = self.records[indexPath.row];

    if([record isKindOfClass:[PlaythroughItem class]]) {
        // ...
    }
    else if([record isKindOfClass:[ReviewItem class]]) {
        // ...
    }
    else if([record isKindOfClass:[TrailerItem class]]) {
        // ...
    }

    return cell;
}

</div>

戰栗吧

這并不是種類型安全的方法,盡管我們在 Objective-C 中這么使用這段代碼已經不足為奇了。在 Swift 中,有更加好的替換方式去解決上述問題,那就是使用枚舉的 case 情況來為不同類型的項做標識,然后通過這些 case 就可以找到我們所需要的項。讓我們看看下面的例子。

例子

這是一個我正在寫的休閑娛樂類 App 中需要一些不同新聞類型的 cell 的代碼:

enum NewsItem {
  case Trailer(index: Int)
  case Review(index: Int)
  case Playthrough(index: Int)
}

</div>

索引僅僅是用來記錄數據在數據庫中 位置 的方法。我們采取這種索引的方法來標識所需數據在 collectionView 中位置的展示。對于特定視頻,我們就不需要其所關聯的所有數據了,所需要的信息僅需要在 collectionView 中的 cell 點擊之后才去通過索引獲取。

我們有一個簡單的 collectionView,它里面含有三個自定義的 cell 。我使用 NewsFeed.swift 文件作為這個新聞 collectionView 的主要數據源。我特別感興趣的是 cellForItemAtIndexPath 方法,通過 NewsItem 枚舉來區分 record 的類型,從而產生相對應的 cell :

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {

    let record = records[indexPath.row]

    switch(record) {

    case .Playthrough(let index): 
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("PlaythroughCell", forIndexPath: indexPath) as! PlaythroughCollectionViewCell
        let playthrough = MediaDB.playthroughAtIndex(index)
        cell.titleLabel.text = playthrough.title
        cell.lengthLabel.text = playthrough.length.prettyTime
        return cell

    case .Review(let index):
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("ReviewCell", forIndexPath: indexPath) as! ReviewCollectionViewCell
        let review = MediaDB.reviewAtIndex(index)
        cell.ratingLabel.text = "\(review.rating) out of 10"
        cell.titleLabel.text = review.title
        return cell

    case .Trailer(let index):
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("TrailerCell", forIndexPath: indexPath) as! TrailerCollectionViewCell
        let trailer = MediaDB.trailerAtIndex(index)
        cell.titleLabel.text = trailer.title
        cell.lengthLabel.text = trailer.length.prettyTime
        return cell
    }
}

</div>

上面的代碼可以清晰看出, record 可以表示為 NewsItem 枚舉里三個 case 中任意一個:

enum NewsItem {
  case Trailer(index: Int)
  case Review(index: Int)
  case Playthrough(index: Int)
}

</div>

當我們想在 collectionView 中展示一個 cell 的時候,我們可以通過相關的索引值去找到數據庫中所對應的那一項。

這段代碼讓我覺得很不舒服。有許多重復代碼,尤其是 switch 顯得非常笨重,在每個 case 中都做了太多事情。

但是,如果我創建了一個可以用在 collectionView cell 上的處理任何數據源的協議呢?鑒于每個視圖(view)都并不相同,所以我不希望這個協議在模型(model)中使用。但我可以在特定的 collectionView cell 的子類上使用它。

所以,我創建了一個叫做 NewsCellPresentable 協議,這個協議被自定義的 collectionView cell 所擴展:

protocol NewsCellPresentable {
    func configureForIndex(index: Int)
}

extension PlaythroughCollectionViewCell: NewsCellPresentable {
    func configureForIndex(index: Int) {
        let playthrough = MediaDB.playthroughAtIndex(index)
        self.titleLabel.text = playthrough.title
        self.lengthLabel.text = playthrough.length.prettyTime
    }
}
extension ReviewCollectionViewCell: NewsCellPresentable {
    func configureForIndex(index: Int) {
        let review = MediaDB.reviewAtIndex(index)
        self.titleLabel.text = review.title
        self.ratingLabel.text = "\(review.rating) out of 10"
    }
}
extension TrailerCollectionViewCell: NewsCellPresentable {
    func configureForIndex(index: Int) {
        let trailer = MediaDB.trailerAtIndex(index)
        self.titleLabel.text = trailer.title
        self.lengthLabel.text = trailer.length.prettyTime
    }
}

</div>

這樣寫看起來已經很簡潔明了了。現在我們回到 cellForItemAtIndexPath 方法中對代碼進行修改,修改后如下所示:

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let record = records[indexPath.row]

    var cell: NewsCellPresentable
    switch(record) {

    case .Playthrough(let index):
        cell = collectionView.dequeueReusableCellWithReuseIdentifier("PlaythroughCell", forIndexPath: indexPath) as! PlaythroughCollectionViewCell
        cell.configureForIndex(index)

    case .Review(let index):
        cell = collectionView.dequeueReusableCellWithReuseIdentifier("ReviewCell", forIndexPath: indexPath) as! ReviewCollectionViewCell
        cell.configureForIndex(index)

    case .Trailer(let index):
        cell = collectionView.dequeueReusableCellWithReuseIdentifier("TrailerCell", forIndexPath: indexPath) as! TrailerCollectionViewCell
        cell.configureForIndex(index)
    }

    return (cell as! MediaCollectionViewCell)
}

</div>

你覺得這種方法怎么樣?這是一種更為簡潔的方法嗎?如果你有其它不同的實現方法,可以直接在文章下面留言給我,或者在 推ter 上留言給我,我的用戶名是 @jquave ,希望可以一起交流學習。

附言

如果你沒有數據庫底層代碼,但又想寫出和我例子一樣的實例,你可以參照下列代碼:

class MediaDB {
    class func titleForRecord(index: Int) -> String {
        return "Title!!"
    }
    class func trailerAtIndex(index: Int) -> Trailer {
        return Trailer()
    }
    class func reviewAtIndex(index: Int) -> Review {
        return Review()
    }
    class func playthroughAtIndex(index: Int) -> Playthrough {
        return Playthrough()
    }
}

struct Trailer {
    let title = "Trailer Title"
    let length = 190
}

struct Review {
    let title = "Review Title"
    let rating = 4
}

struct Playthrough {
    let title = "Playthrough Title"
    let length = 9365
}


enum NewsItem {
    case Trailer(index: Int)
    case Review(index: Int)
    case Playthrough(index: Int)
}

</div>

就個人而言,在寫后端服務和接口之前,我總會做靜態值的存根。這樣會使得項目更容易迭代。

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問http://swift.gg。

 本文由用戶 Candra57X 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!