macoscocoanssplitviewresponder-chain

NSApp.sendAction() only works after some time


I want a ViewController (ContainerVC) to react on clicks on child custom views (ChildView).

In ChildView I override mouseDown(_:) to handle the click. In this method I try to call the ContainerVC target via NSApp.sendAction(#selector(ContainerVC.childViewClicked(_:)), to: nil, from: self).

For some reason the sendAction method fails (i.e. returns false) at first. Also NSApp.target(forAction: #selector(ContainerVC.childViewClicked(_:)) is nil.

After a some time (usually after me wildly clicking on the custom views for some time), the target gets resolved and everything works fine and ContainerVC.childViewClicked(_:) is called.

I couldn't find a systematic pattern after what time/how many clicks the target gets resolved (other than the intensity of me shouting at my mac).

Interestingly it works fine, when I add ContainerVC to a window via let window = NSWindow(contentViewController: ContainerVC()).

The strange behaviour described above occurs when I add ContainerVC to a split view:

self.addSplitViewItem(NSSplitViewItem(viewController: ContainerVC())

I checked the responder chain of CustomView. ContainerVC appears in the chain as expected. There are no other classes in the chain that implement childViewClicked(_:).

I'd appreciate if someone could enlighten me about the inner workings of NSApp.sendAction(_:) and why the target is nil at first.

Are there additional steps necessary when adding a ViewController to a SplitView to wire up things properly?


Solution

  • From the documentation of sendAction(_:to:from:):

    If aTarget is nil, sharedApplication looks for an object that can respond to the message—that is, an object that implements a method matching anAction. It begins with the first responder of the key window.

    When ChildView isn't the first responder, sendAction(_:to:from:) doesn't work. Use

    func `try`(toPerform action: Selector, with object: Any?) -> Bool
    

    for example

    self.`try`(toPerform: #selector(ContainerVC.childViewClicked(_:)), with: self)
    

    which does what you want:

    If the receiver responds to anAction, it invokes the method with anObject as the argument and returns YES. If the receiver doesn’t respond, it sends this message to its next responder with the same selector and object.