7

I'm working with an app in prototype stage of development. Some interface elements do not have any action assigned to them either through storyboard or programmatically.

According to UX guidelines, I want to find these "inactive" buttons in app and have them display a "feature not available" alert when tapped during testing. Can this be done through an extension of UIButton?

How can I assign a default action to UIButton to show an alert unless another action is assigned via interface builder or programmatically?

Alex Stone
  • 46,408
  • 55
  • 231
  • 407

4 Answers4

7

Well what you are trying to achieve can be done. I have done this using a UIViewController extension and adding a closure as the target of a button which does not have a target. In case the button does not have an action an alert is presented.

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.checkButtonAction()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

    }
    @IBAction func btn_Action(_ sender: UIButton) {

    }

}

extension UIViewController{
    func checkButtonAction(){
        for view in self.view.subviews as [UIView] {
            if let btn = view as? UIButton {
                if (btn.allTargets.isEmpty){
                    btn.add(for: .touchUpInside, {
                        let alert = UIAlertController(title: "Test 3", message:"No selector", preferredStyle: UIAlertControllerStyle.alert)

                        // add an action (button)
                        alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))

                        // show the alert
                        self.present(alert, animated: true, completion: nil)
                    })
                }
            }
        }

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

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

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

extension UIControl {
    func add (for controlEvents: UIControlEvents, _ 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 have tested it. Hope this helps. Happy coding.

Md. Ibrahim Hassan
  • 5,359
  • 1
  • 25
  • 45
4

As you cannot override methods in extensions, the only remaining options are:
1. subclass your buttons - but probably not what you're looking for, because I assume you want to use this functionality for already existing buttons
2. method swizzling - to change the implementation of existing function, i.e. init

mag_zbc
  • 6,801
  • 14
  • 40
  • 62
  • 1
    If you end up swizzling you might have to swizzle `initWithCoder` as well, as this would be invoked for buttons added in interface builder. – GoodSp33d Jul 18 '17 at 07:32
  • I see this answer on swizzling, but still don't know how to use that code to assign a selector to buttons. However, my knowledge of swift3 is not enough to make it assign a custom action to buttons. How do I do this? https://stackoverflow.com/questions/39562887/how-to-implement-method-swizzling-swift-3-0 – Alex Stone Jul 18 '17 at 08:35
1

How can I assign a default action to UIButton to show an alert unless another action is assigned via interface builder or programmatically?

I would suggest to do method swizzling:

Through swizzling, the implementation of a method can be replaced with a different one at runtime, by changing the mapping between a specific selector(method) and the function that contains its implementation.

https://www.uraimo.com/2015/10/23/effective-method-swizzling-with-swift/

Remark: I would recommend to check the article above.

As an exact answer for your question, the following code snippet should be -in general- what are you trying to achieve:

class ViewController: UIViewController {
    // MARK:- IBOutlets
    @IBOutlet weak var lblMessage: UILabel!

    // MARK:- IBActions
    @IBAction func applySwizzlingTapped(_ sender: Any) {
        swizzleButtonAction()
    }

    @IBAction func buttonTapped(_ sender: Any) {
        print("Original!")
        lblMessage.text = "Original!"
    }
}

extension ViewController {
    func swizzleButtonAction() {
        let originalSelector = #selector(buttonTapped(_:))
        let swizzledSelector = #selector(swizzledAction(_:))

        let originalMethod = class_getInstanceMethod(ViewController.self, originalSelector)
        let swizzledMethod = class_getInstanceMethod(ViewController.self, swizzledSelector)

        method_exchangeImplementations(originalMethod, swizzledMethod)
    }

    func swizzledAction(_ sender: Any) {
        print("Swizzled!")
        lblMessage.text = "Swizzled!"
    }
}

After calling swizzleButtonAction() -by tapping the "Apply Swizzling" button (applySwizzlingTapped)-, the selector of "Button" should changed from buttonTapped to swizzledAction.


Output:

enter image description here

Ahmad F
  • 30,560
  • 17
  • 97
  • 143
0

I would implement this kind of function by using IBOutlet collection variable, and then removing buttons from that collection as soon as the feature is implemented. something like this:

class ViewController {
    @IBOutlet var inactiveBtns: [UIButton]!

    func viewDidLoad() {
        inactiveBtns.forEach { (button) in
           button.addTarget(self, action: #selector(showDialog), for: UIControlEvents())
        }
    }

    func showDialog() {
        // show the dialog
    }
}

That IBOutlet is visible in the interface builder, so you can add multiple buttons in there.

satellink
  • 499
  • 2
  • 5
  • 10
  • By this way we need to do in all controllers & also have to update if anything modified. – Dharma Jul 18 '17 at 07:43
  • I've considered such solution, but I can't see myself implementing it, as it would touch multiple storyboards and cause merge conflicts with other developers. – Alex Stone Jul 18 '17 at 08:29