Swift实现UIControl绑定触发事件

需求

通常情况下,我们使用target-action模式为UIControl类型的对象设置触发事件:

1
control.addTarget(target, action: Selctor("someAction:"), forControlEvents: event)

很多时候,对于一些触发的事件较为简单的控件,我们希望有一种更简单的方式:

1
2
3
control.trigger({ (sender) -> Void in
//do something
}, onEvent: event)

出于(偷懒)精简代码的目的,我们为UIControl添加扩展来实现这一功能

我们需要实现以下两个接口:

1
2
public func trigger(closure:((UIControl)->Void), onEvent event:UIControlEvents)
public func removeOnEvent(event: UIControlEvents)

实现这一功能的大致的思路如下:

利用字典保存不同的event对应需要触发的事件,为这个事件添加一个target为自身的action,调用另一个方法,该方法从字典中取得所触发事件对应的闭包并调用。


实现

为了保存对应不同event的闭包,我们需要为UIControl添加一个Dictionary属性:

1
2
3
4
5
6
7
8
9
private var bs_closuresDictionary : [UInt : ClosureWrapper<UIControl, Void>]! {
get{
return objc_getAssociatedObject(self, &AssociatedKey.ClosuresDictionary) as? [UInt :ClosureWrapper<UIControl, Void>]
}
set{
let dict = newValue as NSDictionary?
objc_setAssociatedObject(self, &AssociatedKey.ClosuresDictionary, dict, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}

这里用到了关联对象,需要import ObjectiveC模块,定义了一个嵌套struct Asscociated来保存关键字

其中,利用ClosureWrapper类型将Closure封装成对象:

1
2
3
4
5
6
7
8
9
class ClosureWrapper<T, R> {
var _closure: ((T) -> R)?
var closure: ((T) -> R)? {
return self._closure
}
init(_ closure: ((T) -> R)?) {
self._closure = closure
}
}

接下来,就可以实现目标接口了:

1
2
3
4
5
6
7
8
public func trigger(closure:((UIControl)->Void), onEvent event:UIControlEvents){
if self.bs_closuresDictionary == nil {
self.bs_closuresDictionary = [UInt : ClosureWrapper<UIControl, Void>]()
}
self.removeOnEvent(event)
self.bs_closuresDictionary[event.rawValue] = ClosureWrapper<UIControl, Void>(closure);
self.addTarget(self, action: Selector("targetTriggered:forEvent:"), forControlEvents: event)
}

然后实现targetTriggered:forEvent:方法,从字典中获得所触发的UIControlEvents对应的闭包并调用。嗯,看起来似乎就这么结束了。

取得触发的UIControlEvents

这里有一个问题,如何在target中获得当前所触发的UIControlEvents事件?UIContorl中添加action被触发后的回调仅有两个参数:sender和event,无法从中得知是什么事件被触发。
所以,我们需要为每个添加了触发事件的event另外添加一个action,该action在传入的闭包前被调用,记录被触发的event。
由于target-action对应设计模式的原因,我们是无法在action中得到是什么事件被触发的,所以我们需要对每一个UIControlEvents事件都增加一个方法来记录:

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
private class TriggerdEventsHelper {
var _event : UIControlEvents!

var event : UIControlEvents {
return self._event;
}

@objc func touchDownSelector() {
self._event = .TouchDown;
}

@objc func touchDownRepeatSelector() {
self._event = .TouchDownRepeat;
}
// ......
// 剩余的UIControlEvents对应的方法
// ......

func selectorForEvent(event:UIControlEvents) -> Selector {
switch(event){
case UIControlEvents.TouchDown:
return Selector("touchDownSelector")
case UIControlEvents.TouchDownRepeat:
return Selector("touchDownRepeatSelector")
case UIControlEvents.TouchDragInside:
return Selector("touchDragInsideSelector");
// ......
// 剩余的UIControlEvents对应的case
// ......
default:
return nil;
}
}
}

需要注意的是,由于要使用这些方法的动态特性,所以每个用于记录event的方法都要用@objc修饰,更多有关此修饰符可见此处

然后在UIControl的extension中添加该类型的属性:

1
2
3
4
5
6
7
8
private var bs_triggeredEvent : TriggerdEventsHelper!{
get{
return objc_getAssociatedObject(self, &AssociatedKey.triggeredEvent) as? TriggerdEventsHelper
}
set{
objc_setAssociatedObject(self, &AssociatedKey.triggeredEvent, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}

注意事项

接下来就可以直接对所有UIControl对象调用改方法了,不过需要注意的是由于UIControl会持有传入的闭包,在如果某个类持有了该UIControl对象,传入的闭包中如果引用了self,需要在捕获列表中添加通过weak或unowned方式捕获self,防止出现引用循环。