Swift中优雅的为UIButton添加链式的Block点击事件
UIButton是基于 action - target 的事件机制处理点击事件的。通常,如果我们需要添加一个 UIButton 的点击事件的时候,一般会这么做:
btn.addTarget(self, action: #selector(touchUpInSideBtnAction), for: .touchUpInside)
当然,可能对于同一个 button 我们可能会添加不止一个状态的 action ,比如再添加一个状态事件:
btn.addTarget(self, action: #selector(touchUpOutsideBtnAction), for: .touchUpOutside)
除此之外,我们还需要给 button 添加两个接受事件:
@objc func touchUpInSideBtnAction(btn: UIButton) {
print("你好呀","addTouchUpInSideBtnAction")
}
@objc func touchUpOutsideBtnAction(btn: UIButton) {
print("addTouchUpOutsideBtnAction")
}
这种方式将事件相关的分离到另一个地方,虽然高度解耦, 但是,有时候我们只需要点击按钮,然后打印一段小小的内容,并不希望将代码做的这么复杂。聪明的你一下子想到了,将 UIButton 的点击事件做成 Block 不就行了。封装一个 UIButton 的子类,给它添添加一个回调事件的 Block ,再给子类写一个方法,参数带有一个闭包,这个闭包就是 UIButton 被点击时执行的内容。大概就像这样:
typealias BtnAction = (UIButton)->()
class SubButton{
var action : ((UIButton)->())? = nil
// 添加 事件
func DIY_button_add(action:@escaping BtnAction ,for controlEvents: UIControlEvents){
self.action = action
btn.addTarget(self, action: #selector(touchUpInSideBtnAction), for: .touchUpInside)
}
//事件处理
@objc func touchUpInSideBtnAction(btn: UIButton) {
if let act = self.action {
act()
}
}
}
这个办法确实可以,但是稍微有点经验的开发者就会说:如果想给已经存在的 UIButton 添加这个方法该怎么办? 总不可能一个一个的去替换类吧,不仅耗时耗力,关键是容易遗漏。被老大知道要被打的。
你:额....
然后你突然灵光一闪,如果放在 extension 中实现这个方法就行了,这样就不需要创建什么子类了。因为在 extension 并不能添加存储属性,所以你想到了在 extension 中使用 runtime 关联一个属性,这个属性为一个闭包。然后你呼哧呼哧写出了下面的代码:
typealias BtnAction = (UIButton)->()
extension UIButton{
private struct AssociatedKeys{
static var actionKey = "actionKey"
}
@objc dynamic var action: BtnAction? {
set{
objc_setAssociatedObject(self,&AssociatedKeys.actionKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY)
}
get{
if let action = objc_getAssociatedObject(self, &AssociatedKeys.actionKey) as? BtnAction{
return action
}
return nil
}
}
func DIY_button_add(action:@escaping BtnAction) {
self.action = action
self.addTarget(self, action: #selector(touchUpInSideBtnAction), for: .touchUpInside)
}
@objc func touchUpInSideBtnAction(btn: UIButton) {
if let action = self.action {
action()
}
}
}
这时候,你可能有点沾沾自喜了,这简直就是完美嘛! 哈哈。 当然,其实到了这里,你确实已经做完了关于 UIButton 添加 Block 点击事件的大部分工作。
然而,有一天,PM跑过来跟你说,除了 touchUpInside
点击事件,他还想要增加其他的事件状态方法。你也不知道他是不是拆台,他觉得他可能会使用到 UIButton 各个状态的点击状态,比如touchUpOutside
、touchDragOutside
、touchDragInside
等。因为,我们只关联了一个属性,如果是这样,我们可能需要关联很多属性,这不是最麻烦的,更为尴尬的是,有可能我们只需要使用到其中一两个属性,但是却要事先关联好所有的属性。这时候,面对 PM 你有种“大刀饥渴难耐”的感觉了。
最后,你忍住了抽出你80米大刀的冲动。因为你突然想到一个好办法可以很轻松的应付。对,将 extension 中关联的属性设置成一个字典,然后将每个按钮的状态作为 Key ,将每一个状态对应的的动作作为 value 保存起来。每次添加事件,会将对应的事件保存进这个字典中。你的代码改进之后就像这样:
typealias BtnAction = (UIButton)->()
extension UIButton{
/// gei button 添加一个属性 用于记录点击tag
private struct AssociatedKeys{
static var actionKey = "actionKey"
}
@objc dynamic var actionDic: NSMutableDictionary? {
set{
objc_setAssociatedObject(self,&AssociatedKeys.actionKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY)
}
get{
if let dic = objc_getAssociatedObject(self, &AssociatedKeys.actionKey) as? NSDictionary{
return NSMutableDictionary.init(dictionary: dic)
}
return nil
}
}
@objc dynamic fileprivate func DIY_button_add(action:@escaping BtnAction ,for controlEvents: UIControlEvents) {
let eventStr = NSString.init(string: String.init(describing: controlEvents.rawValue))
if let actions = self.actionDic {
actions.setObject(action, forKey: eventStr)
self.actionDic = actions
}else{
self.actionDic = NSMutableDictionary.init(object: action, forKey: eventStr)
}
switch controlEvents {
case .touchUpInside:
self.addTarget(self, action: #selector(touchUpInSideBtnAction), for: .touchUpInside)
case .touchUpOutside:
self.addTarget(self, action: #selector(touchUpOutsideBtnAction), for: .touchUpOutside)
.
.
.
}
}
@objc fileprivate func touchUpInSideBtnAction(btn: UIButton) {
if let actionDic = self.actionDic {
if let touchUpInSideAction = actionDic.object(forKey: String.init(describing: UIControlEvents.touchUpInside.rawValue)) as? BtnAction{
touchUpInSideAction(self)
}
}
}
@objc fileprivate func touchUpOutsideBtnAction(btn: UIButton) {
if let actionDic = self.actionDic {
if let touchUpOutsideBtnAction = actionDic.object(forKey: String.init(describing: UIControlEvents.touchUpOutside.rawValue)) as? BtnAction{
touchUpOutsideBtnAction(self)
}
}
}
}
嗯,虽然代码多了点,但是目前已经很容易就能将所有的状态的点击事件都加入进来了。只要想加入更多的状态的时候,添加对应状态的实现就好了。
最后,为了代码更加的优雅一点,你对上面的代码做了一点点的改变,让每一个点击事件都返回自身,这样就为链式调用形成了可能,这里注意使用 @discardableResult 关键字为函数消除警告:
@discardableResult
func addTouchUpInSideBtnAction(_ action:@escaping BtnAction) -> UIButton{
self.DIY_button_add(action: action, for: .touchUpInside)
return self
}
@discardableResult
func addTouchUpOutSideBtnAction(_ action:@escaping BtnAction) -> UIButton{
self.DIY_button_add(action: action, for: .touchUpOutside)
return self
}
最后,你在项目中使用起来就像这样:
按钮点击事件添加
//测试结果
打印结果
对于如何给 UIView 添加 Block 手势事件也是这样思路。