闭包实现addTarget方法-面向协议编程

Swift 是一门面向协议的编程语言,为什么这么说,请看 WWDC 视频 Protocol-Oriented Programming in Swift

通过闭包的方式为 UIControl 添加 action 的实现方式有很多种,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
extension UIControl {
func listen(_ action: @escaping () -> (), for controlEvents: UIControlEvents) -> AnyObject {
let sleeve = ClosureSleeve(attachTo: self, closure: action, controlEvents: controlEvents)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
return sleeve
}

func listenOnce(_ action: @escaping () -> (), for controlEvents: UIControlEvents) {
let sleeve = ClosureSleeve(attachTo: self, closure: action, controlEvents: controlEvents)
addTarget(sleeve, action: #selector(ClosureSleeve.invokeOnce), for: controlEvents)
}

func unlisten(sleeve: AnyObject) {
guard let sleeve = sleeve as? ClosureSleeve else { return }
self.removeTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: sleeve.controlEvents)
}
}

private class ClosureSleeve {
let closure: () -> ()
let controlEvents:UIControlEvents
let attachedTo: AnyObject

init(attachTo: AnyObject, closure: @escaping () -> (), controlEvents:UIControlEvents) {
self.attachedTo = attachTo
self.closure = closure
self.controlEvents = controlEvents
objc_setAssociatedObject(attachTo, "[\(arc4random())]", self, .OBJC_ASSOCIATION_RETAIN)
}

@objc func invoke() {
closure()
}

@objc func invokeOnce() {
closure()
attachedTo.unlisten(sleeve: self)
}
}

这种实现方式看起来似乎没有问题,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
// Register listener, keep the reference to unregister the listener
let listener = button.listenOnce({
print("I will say this every time you tap the button")
}, for: [.touchUpInside])

// … later …
button.unlisten(listener)

// Listen once for the control events, automatically unlisten when the block is performed
button.listenOnce({
print("I will only say this once")
}, for: [.touchUpInside, .touchDragExit])

使用起来也是简洁可读性好,但是如果我们这样用

1
attachedTo.unlisten(sleeve: self)

会导致崩溃,因为 attachedTo 是一个 AnyObject 类型,如果我们在其他对象上调用的话会出现崩溃的现象。我们也可以用 respondsToSelector 来判断。或者将 attachedTo 变量改为 UIControl,但是如果你想在其他的控件上使用 ClosureSleeve 的话,它将不起作用。这时我们可以使用协议来解决。

在此之前,我们应将闭包包装成一个 class,让闭包能作为关联对象存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// Container class for closures, so that closure can be stored as an associated object
final class ClosureContainer<T: Closurable> {

var closure: (T) -> Void
var sender: T?

init(closure: @escaping (T) -> Void, sender: T?) {
self.closure = closure
self.sender = sender
}

// method for the target action, visible to UIKit classes via @objc
@objc func processHandler() {
if let sender = sender {
closure(sender)
}
}

// target action
var action: Selector { return #selector(processHandler) }
}

接下来我们来实现我们的协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ************** Protocol ***************
/// Closurable protocol
protocol Closurable: class {}
// restrict protocol to only classes => can refer to the class instance in the protocol extension
extension Closurable {

// Create container for closure, store it and return it
func getContainer(for closure: @escaping (Self) -> Void) -> ClosureContainer<Self> {
weak var weakSelf = self
let container = ClosureContainer(closure: closure, sender: weakSelf)
// store the container so that it can be called later, we do not need to explicitly retrieve it.
objc_setAssociatedObject(self, Unmanaged.passUnretained(self).toOpaque(), container, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return container
}
}

如果我想让 UIButton 能够使用闭包进行 addTarget 操作,我们只需要让 UIButton 满足 Closurable 协议即可:

1
2
3
4
5
6
7
extension UIButton: Closurable {

func addTarget(forControlEvents: UIControlEvents = .touchUpInside, closure: @escaping (UIButton) -> Void) {
let container = getContainer(for: closure)
addTarget(container, action: container.action, for: forControlEvents)
}
}

使用方式如下:

1
2
3
4
5
6
7
let btn = UIButton()
btn.addTarget { (sender) in
print("click button")
}
btn.addTarget(forControlEvents: .touchUpInside) { (sender) in
print("click button")
}

这样我们就避免了如上的错误可能,可扩展性也不错。并且我们不仅可以用于 UIControl 类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// extension for UIBarButtonItem - actions with closure
extension UIBarButtonItem: Closurable {

convenience init(image: UIImage?, style: UIBarButtonItemStyle = .plain, closure: @escaping (UIBarButtonItem) -> Void) {
self.init(image: image, style: style, target: nil, action: nil)
let container = getContainer(for: closure)
target = container
action = container.action
}

convenience init(title: String?, style: UIBarButtonItemStyle = .plain, closure: @escaping (UIBarButtonItem) -> Void) {
self.init(title: title, style: style, target: nil, action: nil)
let container = getContainer(for: closure)
target = container
action = container.action
}
}

参考:

  • UIControl+ListenBlock.swift
  • ClosureProtocolExtensions.swift