用 Block 替代 UIButton 的 selector

众所周知,处理 UIButton 的点击事件需要给其所在的 Controller 设置 target ,然后调用 Controller 里对应的方法。这在 MVC 的设计模式下是很符合规范的,因为 View 并不能决定自己要怎么做,它所负责的仅仅是显示出信息,并接受交互事件,所以 View 拿不准的事情或者自己做不到的事情,就要去求助 Controller ,例如 TableView 问控制器:我要显示几组数据?每组里有多少行?每行显示什么 Cell ? 我要开始滚动啦你想做点什么吗?然后控制器才会根据对应的事件作出相应的决定。这分别对应了 delegate 方式和 data source 方式。而 target-action 方式即是用户与 View 交互后,View 询问Controller 需要做什么的一种模式。

虽然这样极大的降低了耦合度,但是也有其缺点:

  • 反复创建新函数很麻烦
  • 会创建很多“污染“代码的一次性函数
  • 代码可读性差
  • selector 写起来容易出错 (因为 Swift 2.3 之前 selector 一直靠字符串寻找方法)

在 Objective-C 里,block 的存在类似于 lambda 表达式,由于 OC 是“深度定制版”的C,所以 block 在编译的时候首先会被解释成C语言中的函数。所以 block 能做到的地方,不用 block 也能做到。

而在 Swift 中,事情就变得比较简单了,因为函数在 Swift 中是一等值(first-class-value),换句话说函数可以作为参数被传递到其他函数中,也可以作为其他函数的返回值。这些特性在一定程度上会给编程工作带来很大便利,也会使代码变得更为简洁优雅。

在 iOS 开发的过程中, 有很多回调往往只需要一次交互,比如网络请求、UIView 动画等等。在这些情况下,使用 block 会使代码整洁得多。目前主流的网络第三方框架中,请求的回调方法无一不是基于 block 的。Apple 在近来加入的 UIAlertViewController 中也采用了用基于 block 的构造形式。所以在一些可以替换的地方,我们可以用 block 代替 target-action,之后不仅用起来很方便,而且代码看起来也整洁得多。所以我们可以自己对 UIButton 进行简单的封装,使之可以使用 block 替代 selector。

首先我们写好一个封装后的函数

typealias DataBlock = (UIButton) -> Void
func addTarget(for controlEvents: UIControlEvents = .touchUpInside, withBlock block: @escaping DataBlock)

我们的思路是接受一个 block 存起来,之后写一个 callback 方法给 selector 接收,最后再在 callback 方法里调用之前存起来的 block。在这里怎么存 block 是一个值得考虑的地方,因为它要能被 callback 方法调用,而且不同的 Button 要有不同的 block, 所以使用属性存起来是最方便的。 在 Swift 中,有一个很好用的关键字 extension。extension 有以下功能:

  • 增加计算实例属性和计算类型属性
  • 定义实例方法和类型方法
  • 提供新的初始化器
  • 定义下标
  • 定义和使用新的内置类型
  • 让一个存在的类型服从一个协议

由于 extension 并不能直接为类定义普通的属性,所以我们给 UIButton 加上一个 DataBlock 类型的计算属性 block,并用运行时给它赋值。 根据这个思路我们可以写出下面的代码

private var block: DataBlock? { 
        set(newValue) { 
               var str = "BlockKey" objcsetAssociatedObject(self, &str, newValue, .OBJCASSOCIATIONCOPYNONATOMIC) }
        get {
               var str = "BlockKey"
               if let block = objc_getAssociatedObject(self, &str) as? DataBlock {
                   return block
               } else {
                   return nil
         }
    }
}
虽没有语法错误,但这段代码还是不能编译通过,错误信息如下:
> While emitting IR SIL function @_TFE9WePeiYangCSo8UIButtons5blockGSqFS0_T__ for setter for block at ClassExtensions.swift:191:9  
>   
> Command failed due to signal: Segmentation fault: 11   

Setter 出了错误,原因是 这个 block 的类型并不属于 AnyObject, 所以不能存储它。所以我们可以给 block 加上一层包装

    private struct associatedKeys {
        static var blockKey = "BlockKey"
    }
      private class BlockContainer: NSObject, NSCopying {
        var block: DataBlock?
        func copy(with zone: NSZone? = nil) -> Any {
            return self
        }
    }
    private var container: BlockContainer? {
        get {
            if let container = objc_getAssociatedObject(self, &associatedKeys.blockKey) as? BlockContainer {
                return container
            }
            return nil
        }
        set(newValue) {
            objc_setAssociatedObject(self, &associatedKeys.blockKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
    }

因为属性关键字有 copy ,所以 BlockContainer 需要遵守 NSCopying 协议,所以要继承 NSObject,实现 copy(with zone) 方法。 而且为了安全,我们把 key 抽象到了一个结构体中。这样就实现了为 UIButton 类添加了一个 container 属性。 之后的工作就很简单了。

        func addTarget(for controlEvents: UIControlEvents = .touchUpInside, withBlock block: @escaping DataBlock) {
        self.container = self.container ?? self.container BlockContainer()
        container?.block = block
        self.addTarget(self, action: #selector(self.callback(sender:)), for: controlEvents)
    }
    
    func callback(sender: UIButton) {
        self. container?.block?(sender)
    }

至此,我们的工作就已经做完了,以后再为 UIButton 添加 target ,就只需要调用简化版的方法即可。

        let button = UIButton()
        button.addTarget { _ in
                print("Button touched up inside")
        }

同理,我们也可以为 UIView 添加同样方法,也可以用 UITapGestureRecognizer 实现,就能使像 UILabel 等本来不支持 addTarget 的 View 方便地添加 target。 同样的,我们也可以实现 block 替代 delegate 或者 data source,对一些轻量级的 View (比如静态 TableView )可能价值会比较大,但是其中的利弊就需要自己权衡了。

Block 是一个很有甜头的语法,如果能够使用得当,代码会变得整洁优雅,但是如果使用不当,会很容易造成内存泄漏的问题,所以在日常的使用中,一定要多加注意循环引用的问题。

参考链接: GitHub – zwaldowski/BlocksKit: The Objective-C block utilities you always wish you had.

 

发表评论