Search code examples
iosswiftuikittarget-action

How to add an action to a UIButton that is in another class


The code below compiles fine, but crashes with an unrecognized selector sent to instance error.

I have one class that inherits from UIViewController:

class Controller: UIViewController {
    override func viewDidLoad() {
        let toolbarWrapper = CustomToolbarWrapper(view: view, target: self)
        let toolbar = toolbarWrapper.toolbarView
        view.addSubview(toolbar)

        ... Other code ...

    }
}

And another class that is just a wrapper for a UIView and contains buttons:

class CustomToolbarWrapper {

    var toolbarView: UIView

    init(view: UIView, target: Any) {
        let height: CGFloat = 80
        toolbarView = UIView(frame: CGRect(x: 0, y: view.frame.height - height, width: view.frame.width, height: height))
        let button = UIButton()

        ... Some button layout code ...

        button.addTarget(target, action: #selector(CustomToolbar.buttonTapped(_:)), for: .touchUpInside)
        toolbarView.addSubview(button)
    }

    @objc static func buttonTapped(_ sender: Any) {
        print("button tapped")
    }
}

For the sake of clarity, I left out a large chunk of code and kept what I thought was necessary. I think that my code doesn't work because of my misunderstanding of the how the target works in the addTarget function. Normally, I would just use self as the target of my button's action, so I just tried to pass along self from the view controller to the CustomToolbarWrapper's init function.

What else I have tried:

Changing the button's target from target to self like this:

button.addTarget(self, action: #selector(CustomToolbar.buttonTapped(_:)), for: .touchUpInside)

results in the app not crashing anymore. Instead, however, I believe that line of code fails to do anything (which doesn't throw an error for some reason?) because attempting to print button.allTargets or even button.allTargets.count results in the app crashing at compile time, with an EXC_BREAKPOINT error and no error description in the console or the XCode UI (which just confuses me even more because there are no breakpoints in my code!).

Also, making buttonPressed(_:) non-static does not change any of the previously mentioned observations.

Also, to make sure the button could in fact be interacted with, I added this in the viewDidLoad() of Controller:

for subview in toolbar.subviews? {
    if let button = subview as? UIButton {
        button.addTarget(self, action: #selector(buttonPressed(_:)), for: .touchUpInside)
    }
}

and added a simple testing method to Controller for the button:

@objc func buttonPressed(_ sender: UIButton) {
    print("Button Pressed")
}

And running the code did result in "Button Pressed" being printed in the console log, so the button should be able to be interacted with by the user.

Feel free to let me know if you think this is not enough code to figure out the problem, and I will post more details.

Edit I prefer to keep the implementation of the button's action in the CustomToolbarWrapper class to prevent repeating code in the future, since the action will be the same no matter where an instance of CustomToolbarWrapper is created.


Solution

  • The best option would be to add the target in your controller and then call a method in your toolbarWrapper on button press. But if you really need to keep this design, you should have a strong reference to your toolbarWrapper in your controller class, otherwise your toolbarWrapper is deallocated and nothing gets called. Also, the buttonTapped(_:) method does not need to be static. Thus, in your controller:

    class Controller: UIViewController {
    
        var toolbarWrapper: CustomToolbarWrapper?
    
        override func viewDidLoad() {
            toolbarWrapper = CustomToolbarWrapper(view: view, target: self)
            let toolbar = toolbarWrapper.toolbarView
            view.addSubview(toolbar)
    
            ... Other code ...
    
        }
    }
    

    And in your wrapper:

    class CustomToolbarWrapper {
    
        var toolbarView: UIView
    
        init(view: UIView, target: Any) {
            let height: CGFloat = 80
            toolbarView = UIView(frame: CGRect(x: 0, y: view.frame.height - height,width: view.frame.width, height: height))
            let button = UIButton()
    
            ... Some button layout code ...
    
            button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
            toolbarView.addSubview(button)
        }
    
        @objc func buttonTapped(_ sender: Any) {
            print("button tapped")
        }
    }