0

My app has numerous buttons whose behavior depends on the events they generate: .touchDown, .touchUpInside, .touchUpOutside, etc.

I use the typical Target-Action mechanism to set these behaviors:

class MyViewController: UIViewController {

     let redButton = UIButton()
     let blueButton = UIButton()

     override func viewDidLoad() {
         super.viewDidLoad()
         self.redButton.addTarget(self, action: #selector(redButtonTouchDown(_:)), for: .touchDown)
         self.redButton.addTarget(self, action: #selector(redButtonTouchUpInside(_:)), for: .touchUpInside)
         self.redButton.addTarget(self, action: #selector(redButtonTouchUpOutside(_:)), for: .touchUpOutside)
         self.blueButton.addTarget(self, action: #selector(blueButtonTouchDown(_:)), for: .touchDown)
         self.blueButton.addTarget(self, action: #selector(blueButtonTouchUpInside(_:)), for: .touchUpInside)
         self.blueButton.addTarget(self, action: #selector(blueButtonTouchUpOutside(_:)), for: .touchUpOutside)
     }

     @objc func redButtonTouchDown(_ sender: UIButton) {
         print("redButton touchDown")
     }

     @objc func redButtonTouchUpInside(_ sender: UIButton) {
         print("redButton touchUpInside")
     }

     @objc func redButtonTouchUpOutside(_ sender: UIButton) {
        print("redButton touchUpOutside")
     }

     @objc func blueButtonTouchDown(_ sender: UIButton) {
         print("blueButton touchDown")
     }

     @objc func blueButtonTouchUpInside(_ sender: UIButton) {
         print("blueButton touchUpInside")
     }

     @objc func blueButtonTouchUpOutside(_ sender: UIButton) {
         print("blueButton touchUpOutside")
     }

 }

This is very verbose. My question is: is there a more efficient way to do this without requiring separate functions for each event type? Here is how the code may look:

class MyViewController: UIViewController {

    let redButton = UIButton()
    let blueButton = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.redButton.addActions(for: [.touchDown, .touchUpInside, .touchUpOutside], #selector(redButtonEvents(_:_:)))
        self.blueButton.addActions(for: [.touchDown, .touchUpInside, .touchUpOutside], #selector(blueButtonEvents(_:_:)))

     }

    func redButtonEvents(_ sender: UIButton, _ event: UIControl.Event) {
        switch event {
        case .touchDown:
            print("redButton touchDown")
        case .touchUpInside:
            print("redButton touchUpInside")
        case .touchUpOutside:
            print("redButton touchUpOutside")
        default:
            break
        }
     }

     func blueButtonEvents(_ sender: UIButton, _ event: UIControl.Event) {
         switch event {
         case .touchDown:
             print("blueButton touchDown")
         case .touchUpInside:
             print("blueButton touchUpInside")
         case .touchUpOutside:
             print("blueButton touchUpOutside")
         default:
            break
         }
      }

   }

I have been looking into various solutions that don't use Selector but instead wrap the method in a closure. I'm afraid I'm not yet experienced enough in obj-c or Swift to see how to refactor my code to do this and also capture my target and object weakly, identify my target, and disambiguate between the senders, etc.

As per this answer: Adding a closure as target to a UIButton

 class ClosureSleeve {
     let closure: ()->()

     init (_ closure: @escaping ()->()) {
         self.closure = closure
     }

     @objc func invoke () {
         closure()
     }
  }

 extension UIControl {
     func addAction(for controlEvents: UIControl.Event, _ closure: @escaping ()->()) {
         let sleeve = ClosureSleeve(closure)
         addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
         objc_setAssociatedObject(self, String(format: "[%d]", arc4random()), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
     }
  }

I'm trying to avoid constantly having to write something like this, which is also quite verbose

 class MyViewController: UIViewController {

      let redButton = UIButton()

      override func viewDidLoad() {
          super.viewDidLoad()

          // this is also quite verbose
          for event in [UIControl.Event]([.touchDown, .touchUpInside, .touchUpOutside]) {
              self.button.addAction(for: event) { [weak self] in
                  if let strongSelf = self {
                      strongSelf.redButtonActions(strongSelf.button, event)
                  }
              }
          }              
     }

     func redButtonActions(_ sender: UIButton, _ event: UIControl.Event) {
         // handle red button actions
     }
}

Help is greatly appreciated. Thank you.

MH175
  • 2,234
  • 1
  • 19
  • 35
  • Well, the problem is that there is no way for a control event action handler to know what control event triggered the handler call. That's just the way it is. It's a deep, ingrained flaw in the control event target-action mechanism. So the workarounds you've shown are the workarounds there are. – matt Feb 10 '19 at 02:17
  • Thanks matt. Hm, maybe I'm not phrasing my post right. It's not so much the workaround itself as much as it seems there should be a way to reduce my typing load. In other words, it seems I should be able to add a few arguments to the ClosureSleeve that will allow me to create a helper function which can automatically pass in the sender, event, and weak references, et al. I will keep looking and try to rephrase the question. – MH175 Feb 10 '19 at 19:09
  • Well, it's difficult to say more because you are not showing sufficient real code to reproduce the issue and you're not explaining what the actual issue is. "I can't seem to figure the intricacies" is not a question, and the example you give calls a method `buttonActions` that you have not defined for us. – matt Feb 10 '19 at 21:46

0 Answers0