写更优雅的 Swift 框架 - rx_tap -> rx.tap

前几天在 RxSwift 项目下看到了个 Issue 826 (opens new window) 。我表示对这个 Issue 很感兴趣,用一句话概括,JegnuX (opens new window) 希望调整 Swift 3 版本的 RxSwift 中对应 Cocoa 扩展的 API,比如将 rx_tap 替换成 rx.tap

# 对扩展方法的讨论

先来概述一下 JegnuX 的描述。

当然,你可以直接看 Issue 826 (opens new window)

JegnuX 指出用 rx_ 前缀作为 Cocoa 的扩展不是非常优雅的风格,而且这并不适合 Swift 代码风格。无需怀疑的是为一个类添加一个方法是需要前缀的,具体可以参见瞄大的如何打造一个让人愉快的框架 (opens new window)。可供选择的前缀有。

  • rx_tap,目前最常见的方案。
  • rxTap,使用标准的驼峰式写法,然而这很困惑,这不能很好的表达这是一个 rx 的扩展方法,很容易误解为某个一般的方法。
  • reactiveTap,相比 rxTap 增加了易读性,然而并没有什么卵用。
  • rx.tap,卧槽,如此完美,用命名空间的形式表达了这是一个属于 rx 的扩展方法

为什么前缀 rx. 就是一个好的设计?

主要一点就是这里用了类似代理 proxy 的方法,具体的实现内容在对应的 proxy 方法中。

同时 Swift 标准库中 LazySequence 也是采用了类似方案。比如。

myArray.map { ... }
myArray.lazy.map { ... }

在 RxCocoa 中我们也可以采用相似的语法。

myButton.rx.tap.subscribeNext { ... }

JegnuX 曾写了一篇 safe-collection-subsripting-in-swift (opens new window),解释为什么 myArray.safe[2]myArray[safe: 2] 更好,同时给出了自己的实现方案。

此外,JegnuX 还在 Issue 826 (opens new window) 中给出了代码实现的细节,以及在内存管理上要注意的问题。

我在这里也总结了一些优点。

# 代码结构更清晰

来对比一下 rx_taprx.tap 的区别。

rx_tap 就是一个属性,UIButton 的一个属性,再去理解一下属性名,才可以知道这应该是 Rx 的一个扩展属性。

rx.tap 就不同了,分开来看,rxUIButton 的一个属性,tap 是 Rx<UIButton> 的一个属性,这样一来就很清晰的表明了。

  • UIButton 支持 Rx 扩展
  • RxbaseUIButton 时,具有一个 tap 属性

此外,Swift 标准库中所有的 public 方法都没有采用下划线的方式进行区分代码。正如 JegnuX 描述的,lazy 是一个扩展方法系列,这里存在一个层级的感觉。array -> lazy -> map ,而非 array -> map: lazy 的形式。而这里的 rx_tap 就是 button -> tap: rxrx.tap 就变成了 button -> rx -> tap

# 更优雅的自动补全

这里以之前在链家做的 Swift 下的 UITableView (opens new window) 分享为例,我用泛型加协议写了一个扩展方法,方便注册 Cell 和重用 Cell 。类似的方法可以参考使用泛型来优化 TableView Cells 的使用体验 (opens new window)

我写的扩展方法如下。

public extension UITableView {
    public func st_dequeueReusableCell<Cell: UITableViewCell where Cell: IdentifiableType>(withIdentifierable identifierable: Cell.Type, for indexPath: NSIndexPath) -> Cell {
        return dequeueReusableCellWithIdentifier(identifierable.identifier, forIndexPath: indexPath) as! Cell
	  }
}

考虑到这可能作为框架使用,我加上了 st_ 前缀表达这是一个属于 Swifty 框架的方法。此时如果我写了个 s***t 的方法,这个方法也可能在自动补全中提示出来,这很尴尬,很明显这不是我们想要的。换到 st. 的形式就没问题了。

优雅的自动补全

嗯,这很优雅。唯一可惜的是,这在 Xcode 7 中存在一个 bug ,比如我在使用 Label 的某个 st 系列方法时,自动补全的情况变得很不乐观。

错误的代码提示

显然我写的扩展方法是没有问题的。

extension Swifty where Base: UITableView {
    public func dequeueReusableCell<Cell: UITableViewCell where Cell: IdentifiableType>(withIdentifierable identifierable: Cell.Type, for indexPath: NSIndexPath) -> Cell {
	      return base.dequeueReusableCellWithIdentifier(identifierable.identifier, forIndexPath: indexPath) as! Cell
	  }
}

我限定了该方法只能用在 UITableView 下。我在 Xcode 8 beta 5 下写了一遍,没有什么问题,正确的提示,这应该是 Xcode 7 的 bug 。

Xcode 8 下的补全 1 Xcode 8 下的补全 2

此外,采用这样的写法会减少 Xcode 崩溃,毕竟自动补全的方法搜索范围大大减少了。

# 实现细节

用一个 stuct/class 将需要处理的 class 封装一下就可以。

public struct Swifty<Base> {
    public let base: Base
    public init(_ base: Base) {
        self.base = base
    }
}

添加扩展属性。

public extension NSObjectProtocol {
    public var st: Swifty<Self> {
        return Swifty(self)
    }
}

之后只需要为 Swifty 添加 extension

extension Swifty where Base: UITableView {
	//...
}

_ 迁移到 . 是件很容易的事情。

# 第三方 Swift 框架的发展

rx.tap 是更优雅的写法,相比从 Objective-C 时代留下来的“下划线”方法将在 Swift 框架中逐渐消失,RxSwift 的 Swift 3 版本已经决定采用了该方案,此外需要注意的是,如果你有使用 SnapKit ,可能会好奇为什么 SnapKit (opens new window) 为什么迟迟没有出 Swift 3 分支,其实 feature/0.40.0 (opens new window) 分支就是对应的 Swift 3 分支,而语法的变动也从 _ 变成了 .

box.snp.makeConstraints { (make) -> Void in
   make.width.height.equalTo(50)
   make.center.equalTo(self.view)
}