Assigning a button’s action – with added bug

Edited to add:
This, apparently, works as intended – you need to assign the button’s _target_ with = self.
It doesn’t completely explain why the code is working under some circumstances, but it’s not a bug as such.

This thoroughly spoilt my day today. I was convinced I’d gotten the ‘assign-an-action-on-the-fly’ code correctly, but it just Would. Not. Work. Turns out, I had gotten it right.

0) Create a new macOS application. Create a new NSViewController subclass, call it ‘EmbedController’; set it as the class of the initial view controller. Add one button to it, and add the following code:

class EmbedController: NSViewController {
override func viewDidLoad() {
testButton.action = #selector(EmbedController.simpleTest(sender:))

@IBAction func simpleTest(sender: NSButton){
print("This is a test")

@IBOutlet weak var testButton: NSButton!

Connect the IBOutlet to the button; do NOT connect the IBAction.

Build and run. The button’s action gets assigned in viewDidLoad; if you press the button, “This is a test” will appear in the console.

1) Disconnect the EmbedController from the window. Create a new ViewController scene, add a container view, and create an embed segue from the containerView to the EmbedController.

Build and run. The button does nothing at all.

2) Add another IBAction to the button and connect it in IB:

@IBAction func actionToOverride(_ sender: NSButton) {
print("This should not be visible")

Build and run again. This time, simpleTest fires and “This is a test” appears in the console.

My best bet is that setting an action in InterfactBuilder causes the button to be created slightly earlier so that the action gets assigned correctly? Or something along those lines?

3) As a bonus, I have tested this with other forms of embedding: NSTabViewController, NSSplitViewController – the behaviour is consistent. If you want to set an action in code, you need to have an action connected in IB.