面向协议的UITableViewCells

面向协议的UITableViewCells

这是我基于英文原文翻译的译文,如果你对本文感兴趣而且想转发,你应该在转发文章里加上本文的链接

英文原文链接

这篇文章将展示如何通过面向协议编程(POP)而不是通过类继承,来实现不同UITableViewCell对象的展示。准备好了吗?让我们一起来看看具体怎么做!

cells展示!

当我开发Cast Player这个app的时候,我需要添加很多设置页面来让用户调整app的一些设置,其中就包括一个反馈表格和一些关于界面。下面是这个界面的展示:

从上面的界面展示可以看到,我们要定义6个不同类型的cells:

总而言之,这些cells都有以下的特点(或行为):

  • Cell 高亮
  • 展示一个标题
  • 展示一个可读的计数

我们怎么创建能代表这6种不同类型的cells类呢?第一步应该要做要做的,就是使用表格列出所有cell的类型和特点。

从表格中我们可以看到:

  • 没有一个特性满足所有的cells。
  • 一些特性满足一些cells,但是并不满足其他的cells

这就意味着通过类继承来实现一个UITableViewCell并不是一个好的选择。事实上,这些cell 类型中没有一个适合作为基类。你认为呢?🤔

协议 & 扩展!Yay!

幸运的,这篇文章给我们提供了一个解决的思路。对于这种使用场景,协议和扩展将会非常有用,但是应该怎么去使用它们呢?

这个做法是这样的,我们可以使用协议为每一个特性定义接口,并通过扩展提供接口默认的实现。然后,我们可以创建一个轻量级的UITableViewCell子类,并让它遵循所需要的接口规范。在一篇叫《面向协议的MVVM介绍》中有一个非常类似的问题。我们可以看看这个具体是怎么做的!

使用协议和扩展,我们可以写第一个特性的代码,TitlePresentable:

protocol TitlePresentable {
  var titleLabel: UILabel! { get set }
  func setTitle(title: String?)
}

extension TitlePresentable {
  func setTitle(title: String?) {
    titleLabel.text = title
  }
}

第二个特性,BytesCountPresentable使用一样的模式:

protocol BytesCountPresentable {
  var bytesCountLabel: UILabel! { get set }
  func setBytesCount(bytesCount: Int64?)
}

extension BytesCountPresentable {
  func setBytesCount(bytesCount: Int64?) {
    bytesCountLabel.text = bytesCountString(bytesCount)
  }
  private func bytesCountString(bytesCount: Int64?) -> String {
    if let bytesCount = bytesCount {
      return bytesCount.bytesFormattedString() // defined elsewhere
    }
    return "N/A"
  }
}

通过让这两个特性分成两个不同的协议,BytesCountTitleTableViewCell的实现就变得简单了。

class BytesCountTitleTableViewCell: UITableViewCell, TitlePresentable, BytesCountPresentable {
  @IBOutlet var titleLabel: UILabel!
  @IBOutlet var bytesCountLabel: UILabel!
}

通过遵循TitlePresentable和BytesCountPresentable协议,BytesCountTitleTableViewCell能够继承通过扩展添加的行为。其他类可以通过一样的接口来继承同样的行为。这真的非常强大!💪💪

注意到类需要重新声明titleLabel和bytesCountLabel两个属性,以便于能够实现对应的协议,但我们也需要指定这些IBOutlet变量,方便我们可以通过Interface Builder来链接它们。

highlighting特性怎么办?

我们先从这个类开始:

class HighlightableTableViewCell: UITableViewCell {
  override func setHighlighted(highlighted: Bool, animated: Bool) {
    self.contentView.backgroundColor = highlighted ? UIColor(white: 217.0/255.0, alpha: 1.0) : nil
  }
}

这里我们选择这样做,通过复写UITablViewCell的setHighlighted()方法来实现cell 高亮。

根据上面总结的方法,我们可以定义一个HighlightableView的协议和扩展:

protocol HighlightableView {
  func setHighlighted(highlighted: Bool, animated: Bool)
}

extension HighlightableView {
  func setHighlighted(highlighted: Bool, animated: Bool) {
    print("extension highlighted") // not printed
  }
}

我们可以尝试创建一个实现HighlightableView 协议的UITableViewCell的子类,并看看高亮特性是否可以实现。但这次没那么幸运了。🚫

我认为这是因为当setHighlighted()方法被调用的时候,那些UITableViewCell的基类方法将会被调用,而不会调用协议扩展方法。

在一般情况下,协议扩展是为了在已有的类中加入新的行为,而不能复写已有方法。

为了Cell的高亮,再来一遍!

为了让cell的高亮特性能正常展示,如果必要的话,我们可以保留HighlightableTableViewCell的定义并从它那里派生出一个子类。例如:

class DisclosureTitleTableViewCell: HighlightableTableViewCell, TitlePresentable {
  @IBOutlet var titleLabel: UILabel!
}

class DisclosureBytesCountTitleTableViewCell: HighlightableTableViewCell, TitlePresentable, BytesCountPresentable {
  @IBOutlet var titleLabel: UILabel!
  @IBOutlet var bytesCountLabel: UILabel!
}

当我们选择要使用HighlightableTableViewCell或UITableViewCell作为基类时,取决于我们是否需要cell高亮特性,并根据需要使用TitlePresentable和BytesCountPresentable的特性。

类的层级如下图所示:

在代码中是这样的:

class HighlightableTableViewCell: UITableViewCell
 
class DisclosureTitleTableViewCell: HighlightableTableViewCell, TitlePresentable
 
class BytesCountTitleTableViewCell: UITableViewCell, TitlePresentable, BytesCountPresentable
 
class DisclosureBytesCountTitleTableViewCell: HighlightableTableViewCell, TitlePresentable, BytesCountPresentable
 
class ActionTitleTableViewCell: HighlightableTableViewCell, TitlePresentable
 
class ValuePickerTableViewCell: UITableViewCell
 
class ValuePickerLabelTableViewCell: ValuePickerTableViewCell, TitlePresentable

注意:HighlightableTableViewCell 是唯一的通过子类实现,并充当其他三个额外cell类型基类的类。一旦一个给定的类型被选择作为基类,额外的特性只能通过协议扩展添加。

结论

Swift的协议和扩展,是一种能添加行为(特性)的方法。这种方法在类设计上很有效,能保持类层级扁平化。🚀主要优势如下:

  • 更扁平化的类层级
  • API/类更容易扩展
  • 相对修改代码,修改协议更容易
  • 更少的代码重复

反馈

文章中展示的解决方法,对于我和我的特殊使用场景来说非常有效果 - 我希望这个实践例子能够帮助大家理解协议扩展是怎么使用的,最好对比下我刚开始做的那个继承例子。如果你知道更好的实现方法,可以在我的评论下留言。

注意:这篇文章最早发表于我的博客中,时间为2016年6月18日。

如果想看更多类似的文章,请订阅我的邮件订阅

如果你喜欢这篇文章,请点击喜欢(💚)以便让更多人在Medium中看到。

还有,不要忘记下载我的Cast Player应用哦. 😇

如果你喜欢这篇译文,记得在简书中给我点赞并关注我哦

本文Github地址

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

推荐阅读更多精彩内容