使用泛型来优化 TableView Cells 的使用体验

作者:Olivier Halligon,原文链接,原文日期:2016-01-06
译者:walkingway;校对:小锅;定稿:numbbbbb

各位新年快乐🎇🎉🎊🎆! 2016 年第一篇博客我想分享一个非常有用的技巧,那就是向大家展示 Swift 泛型的强大,以及方便地使用泛型来处理 UITableViewCellsUICollectionViewCells

介绍

我不喜欢使用字符串做标识符,我认为使用常量要比字符串好很多。

但是,当涉及到 UITableViewCellUICollectionViewCell 以及他们的重用标识符(reuseIdentifiers)时,我想采用一种更加魔幻的解决方案:『使用 Swift 的泛型 + Mixins 的方式』,下面让我们摒住呼吸,见证奇迹的时刻。

魔法时刻

我的想法是在 UITableViewCell(或 UICollectionViewCell)的子类中将 reuseIdentifier 声明为一个静态常量,然后用它使这个 cell 的实例对外部透明化(即,不用显式地使用 reuseIdentifer 来实例化 cell)。

我们首先声明一个协议,以便于稍后能够将其作为 Mixin 来使用:

protocol Reusable: class {
  static var reuseIdentifier: String { get }
}

extension Reusable {
  static var reuseIdentifier: String {
     // 我喜欢使用类名来作为标识符
     // 所以这里可以用类名返回一个默认值
    return String(Self)
  }
}

当我们使用泛型实现 dequeueReusableCell(…) 方法时,魔法出现了:

func dequeueReusableCell<T: Reusable>(indexPath indexPath: NSIndexPath) -> T {
  return self.dequeueReusableCellWithIdentifier(T.reuseIdentifier, forIndexPath: indexPath) as! T
}

得益于 Swift 的类型推断,这个方法将使用调用点的上下文来推断 T 的实际类型,因此这个类型可以被看做方法实现中的『复古注入』(retro-injected)!

let cell = tableView.dequeueReusableCell(indexPath: indexPath) as MyCustomCell

注意观察 reuseIdentifier 在内部的使用方法...完全由编译器看到的返回类型决定!那就是我说的类型“复古注射(retro-injected)”,以及为什么我超😍它的原因

译者注:Swift 的类型在编译时刻就确定了,所以当你写下 as MyCustomCell 后,cell 的类型即泛型 T 的具体类型就确定了

很美妙不是吗?

更进一步

当然,除了 dequeueReusableCellWithIdentifier 之外,你同样可以把这个方法用在 registerNib(_, forCellWithReuseIdentifier:)UICollectionViewCells,supplementary 视图上。

UITableViewCellsUICollectionViewCells 都能通过类名(registerClass(_, forCellWithReuseIdentifier:)) 或 nib(registerNib(_, forCellWithReuseIdentifier:))的方式进行注册。如果存在 nib,我们将在协议中添加一个类型属性(static var nib: UINib?),然后使用这个 nib 注册 cell;如果 nib 不存在则使用类注册。

代码

这里是我在项目中实际使用的代码:

[2016-01-20 修改]

现在可以在Github上看到这个代码了!

import UIKit

protocol Reusable: class {
  static var reuseIdentifier: String { get }
  static var nib: UINib? { get }
}

extension Reusable {
  static var reuseIdentifier: String { return String(Self) }
  static var nib: UINib? { return nil }
}

extension UITableView {
  func registerReusableCell<T: UITableViewCell where T: Reusable>(_: T.Type) {
    if let nib = T.nib {
      self.registerNib(nib, forCellReuseIdentifier: T.reuseIdentifier)
    } else {
      self.registerClass(T.self, forCellReuseIdentifier: T.reuseIdentifier)
    }
  }

  func dequeueReusableCell<T: UITableViewCell where T: Reusable>(indexPath indexPath: NSIndexPath) -> T {
    return self.dequeueReusableCellWithIdentifier(T.reuseIdentifier, forIndexPath: indexPath) as! T
  }

  func registerReusableHeaderFooterView<T: UITableViewHeaderFooterView where T: Reusable>(_: T.Type) {
    if let nib = T.nib {
      self.registerNib(nib, forHeaderFooterViewReuseIdentifier: T.reuseIdentifier)
    } else {
      self.registerClass(T.self, forHeaderFooterViewReuseIdentifier: T.reuseIdentifier)
    }
  }

  func dequeueReusableHeaderFooterView<T: UITableViewHeaderFooterView where T: Reusable>() -> T? {
    return self.dequeueReusableHeaderFooterViewWithIdentifier(T.reuseIdentifier) as! T?
  }
}

extension UICollectionView {
  func registerReusableCell<T: UICollectionViewCell where T: Reusable>(_: T.Type) {
    if let nib = T.nib {
      self.registerNib(nib, forCellWithReuseIdentifier: T.reuseIdentifier)
    } else {
      self.registerClass(T.self, forCellWithReuseIdentifier: T.reuseIdentifier)
    }
  }

  func dequeueReusableCell<T: UICollectionViewCell where T: Reusable>(indexPath indexPath: NSIndexPath) -> T {
    return self.dequeueReusableCellWithReuseIdentifier(T.reuseIdentifier, forIndexPath: indexPath) as! T
  }

  func registerReusableSupplementaryView<T: Reusable>(elementKind: String, _: T.Type) {
    if let nib = T.nib {
      self.registerNib(nib, forSupplementaryViewOfKind: elementKind, withReuseIdentifier: T.reuseIdentifier)
    } else {
      self.registerClass(T.self, forSupplementaryViewOfKind: elementKind, withReuseIdentifier: T.reuseIdentifier)
    }
  }

  func dequeueReusableSupplementaryView<T: UICollectionViewCell where T: Reusable>(elementKind: String, indexPath: NSIndexPath) -> T {
    return self.dequeueReusableSupplementaryViewOfKind(elementKind, withReuseIdentifier: T.reuseIdentifier, forIndexPath: indexPath) as! T
  }
}

示例用法

下面演示如何声明一个 UITableViewCell 的子类:

class CodeBasedCustomCell: UITableViewCell, Reusable {
  // By default this cell will have a reuseIdentifier or "MyCustomCell"
  // unless you provide an alternative implementation of `var reuseIdentifier`
  // ...
}

class NibBasedCustomCell: UITableViewCell, Reusable {
  // Here we provide a nib for this cell class
  // (instead of relying of the protocol's default implementation)
  static var nib: UINib? {
    return UINib(nibName: String(NibBasedCustomCell.self), bundle: nil)
  }
  // ...
}

然后在 UITableViewDelegate/UITableViewDataSource 中使用他们:

class MyTableViewController: UITableViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    tableView.registerReusableCell(CodeBasedCustomCell.self) // This will register using the class without using a UINib
    tableView.registerReusableCell(NibBasedCustomCell.self) // This will register using NibBasedCustomCell.xib
  }

  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell: UITableViewCell
    if indexPath.section == 0 {
      cell = tableView.dequeueReusableCell(indexPath: indexPath) as CodeBasedCustomCell
    } else {
      cell = tableView.dequeueReusableCell(indexPath: indexPath) as NibBasedCustomCell
    }
    return cell
  }
}

另一种解决方案

有些人倾向于将 Reusable 协议拆分成两个不同的协议,用于区分基于 nib 创建与基于 class 创建的 cell:

protocol Reusable: class {
  static var reuseIdentifier: String { get }
}
extension Reusable {
  static var reuseIdentifier: String { return String(Self) }
}

protocol NibReusable: Reusable {
  static var nib: UINib { get }
}
extension NibReusable {
  static var nib: UINib {
    return UINib(nibName: String(Self), bundle: nil)
  }
}

这样基于 nib 创建的 cell 也能使用默认实现 —— 因此不用在子类中再重新实现一遍。

但是这也会迫使你在 UITableViewUICollectionView上添加更多的实现方法(每个协议中添加一个实现),所以嘛...这其中的平衡还要靠你自己来把握 ⚖😉

福利:现在你能通过 Cocoapods 使用这个库啦 🎉

2016-01-20 增加

以上代码和示例,我已经上传至 GitHub,并且通过 Swift Package 以及 CocoaPod 发布了,现在可以很方便地添加到你的工程中。

随时欢迎各种 PR 来共同改进这个项目 😉

--

最后希望你喜欢这一技巧,我们下次再见喽!🎉

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,066评论 4 62
  • 定义:background-size 属性规定背景图像的尺寸。 兼容性:IE9+、Firefox 4+、Opera...
    Maggie_77阅读 1,310评论 0 0
  • 昆明的天,时常都是蓝蓝的,漂着几朵白色的云,明朗的不像话。 那一年,我拖着行李箱上了去成都的火车,一路...
    闻是然也阅读 237评论 0 0
  • 我,我是来道歉的,我不该把两颗星星和手链搞丢的,之前不敢跟你说,怪我自己太大意……可是掉了之后真的很伤心,可我也不...
    asdfhhjj阅读 401评论 0 0
  • ​ 原来他是《爆裂鼓手》的导演,他执导的影片都在闪闪发光,他的电影里总在讲述梦想,LA LA LAND依然。 梦想...
    午夜石榴阅读 617评论 0 1