2

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.

RPatel99
  • 7,448
  • 2
  • 37
  • 45

3 Answers3

1

Your problem is right here:

let toolbarWrapper = CustomToolbarWrapper(view: view, target: self)

You're passing an instance of Controller class which doesn't implement the buttonTapped(_:) selector. It is implemented by your CustomToolbarWrapper class. This is a bad design in general. You should either follow a delegate pattern, or a callback pattern.

Updated Answer:

Delegate pattern solution:

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

    // MARK: - CustomToolbarWrapperDelegate
    func buttonTapped(inToolbar toolbar: CustomToolbarWrapper) {
        print("button tapped")
    }
}

protocol CustomToolbarWrapperDelegate: AnyObject {
    func buttonTapped(inToolbar toolbar: CustomToolbarWrapper) -> Void
}

class CustomToolbarWrapper {

    var toolbarView: UIView
    weak var buttonDelegate: CustomToolbarWrapperDelegate?

    init(view: UIView, buttonDelegate: CustomToolbarWrapperDelegate?) {
        let height: CGFloat = 80
        toolbarView = UIView(frame: CGRect(x: 0, y: view.frame.height - height, width: view.frame.width, height: height))
        self.buttonDelegate = buttonDelegate
        let button = UIButton()
        button.addTarget(self, action: #selector(self.buttonTapped(_:)), for: .touchUpInside)
        toolbarView.addSubview(button)
    }

    @objc private func buttonTapped(_ sender: Any) {
        // Your button's logic here. Then call the delegate:
        self.buttonDelegate?.buttonTapped(inToolbar: self)
    }

}

If you'd rather stick to your current design then just implement the following changes:

class Controller: UIViewController {
    override func viewDidLoad() {
        let toolbarWrapper = CustomToolbarWrapper(view: view, target: self, selector: #selector(self.buttonTapped(_:)), events: .touchUpInside)
        let toolbar = toolbarWrapper.toolbarView
        view.addSubview(toolbar)
    }

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

class CustomToolbarWrapper {

    var toolbarView: UIView

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

        button.addTarget(target, action: selector, for: events)
        toolbarView.addSubview(button)
    }

}
Pranay
  • 846
  • 6
  • 10
  • An instance of `CustomToolbarWrapper` will have a button with the same action regardless of what class the instance is located in, which is why I'd rather implement the button's action in `CustomToolbarWrapper`. Otherwise I would have to copy and paste the same `buttonTapped` function in every view controller that has an instance of `CustomToolbarWrapper`. – RPatel99 Jan 08 '19 at 01:34
  • @RPatel99 Makes sense. In that case, I'd suggest you go with delegate pattern. Check out my updated answer – Pranay Jan 08 '19 at 01:53
  • One of the other answers fixed my problem without me having to implement a delegate, so I marked it as correct, but using a delegate is probably still better convention even if does seem a little convoluted, so I'll still upvote this. – RPatel99 Jan 08 '19 at 02:23
1

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")
    }
}
alanpaivaa
  • 1,959
  • 1
  • 14
  • 23
1

There is another way I would use which is delegation. The target does not necessarily have to be a controller, it can be the CustomToolbarWrapper itself. First, declare a protocol

protocol CTDelegate: AnyObject {
  func didClickButton()
}

Then in CustomToolbarWrapper add a property, weak var delegate: CTDelegate? and a button action:

@objc func buttonTapped(_ sender: UIButton) {

   delegate?.didClickButton()
}

So in your case, it becomes:

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

Then when you go to any ViewController, conform to CTDelegate and initialize the CustomToolbarWrapper, you can set its delegate to the controller. e.g

let toolbarWrapper = CustomToolbarWrapper(view: view, target: self)
toolbarWrapper.delegate = self

and implement your action inside the method you are conforming to in your controller i.e.

func didClickButton()
Kwaku Eshun
  • 151
  • 2
  • 7
  • This is interesting. I didn't consider using a delegate, but I need to implement my action in `CustomToolbarWrapper`, because otherwise I'd have to copy/paste the same code between multiple view controllers. Could I create an instance of the delegate in the view controller and implement the delegate in `CustomToolbarWrapper`? Would that work as intended? – RPatel99 Jan 08 '19 at 01:30
  • The delegate is meant to make the button press action not dependent on tool bar wrapper but whoever implements. Unless, I am mistaken about your initial intentions – Kwaku Eshun Jan 08 '19 at 01:37
  • I think you can implement it inside an extension of the delegate protocol to prevent repeating the code – Kwaku Eshun Jan 08 '19 at 01:39
  • Okay I'll try that. – RPatel99 Jan 08 '19 at 01:40
  • Yeah per my post, `buttonTapped` is in `CustomToolbarWrapper` – Kwaku Eshun Jan 08 '19 at 01:42
  • Yeah, but you said to put `didClickButton()` in the controller, and that function is supposed to have all the implementation of the button's action. – RPatel99 Jan 08 '19 at 01:48
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/186330/discussion-between-kwaku-eshun-and-rpatel99). – Kwaku Eshun Jan 08 '19 at 01:50