Swift见解有些文章不一定是为了上首页投稿iOS学习笔记

Swift中优雅的为UIButton添加链式的Block点击事件

2018-04-14  本文已影响80人  BennyLoo

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 各个状态的点击状态,比如touchUpOutsidetouchDragOutsidetouchDragInside等。因为,我们只关联了一个属性,如果是这样,我们可能需要关联很多属性,这不是最麻烦的,更为尴尬的是,有可能我们只需要使用到其中一两个属性,但是却要事先关联好所有的属性。这时候,面对 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 手势事件也是这样思路。

上一篇下一篇

猜你喜欢

热点阅读