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 各个状态的点击状态,比如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 手势事件也是这样思路。

PS:文章中的代码基于Swift 3.0。后续 Swift 版本更新,可能会出现崩溃现象或者错误代码提示。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,642评论 18 139
  • 这一天,是难忘的。清晨5点多,踏着冰雪路,拉着笨重的行李箱,走在离开家的村道上,每一步都很艰难,艰难中包含着...
    释若读书阅读 1,012评论 5 18
  • 灰色的身上有着深深的皱纹,好像智慧的长者脸上的皱纹。大大的耳朵像两把蒲扇紧紧贴着身体。灰色的身体像一座大山...
    Roxanne_65阅读 270评论 0 0
  • 昨夜梦见你,还是白嫩狡黠的少年模样。你亲手剥了瓣酸桔给我,笑嘻嘻地向我表白,认真而偏执。我的诸般繁杂心绪如淙...
    枕南音阅读 373评论 0 0
  • 二妹小妹的两个孩子,今年都中考。现在复习阶段,很是紧张,特别是4月份的就要进行体育考试。 两个妹妹都出差在...
    星空_9e01阅读 222评论 0 1