Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cocoa Storyboard Responder Chain

Storyboards for Cocoa apps seems like a great solution as I prefer the methodology you find in iOS. However, while breaking things up into separate view controllers makes a lot of logical sense, I'm not clear as to how to pass window control (toolbar buttons) or menu interaction down to the view controllers that care. My app delegate is the first responder and it receives the the menu or toolbar actions, however, how can I access the view controller that I need to get that message to? Can you just drill down into the view controllers hierarchy. If so, how do you get there from the app delegate since it's the first responder? Can you make the window controller the first responder instead. If so, how? In the storyboard? Where?

Since this is a high level question it may not matter, however, I am using Swift for this project if you're wondering.

like image 571
Matt Long Avatar asked Nov 04 '14 20:11

Matt Long


2 Answers

I'm not sure if there is a "proper" way to solve this, however, I have come up with a solution that I'll use for now. First a couple of details

  • My app is a document based application so each window has an instance of the document.

  • The document the app uses can act as the first responder and forward any actions I've connected

  • The document is able to get a hold of the top level window controller and from there I am able to drill down through the view controller hierarchy to get to the view controller I need.

So, in my windowDidLoad on the window controller, I do this:

override func windowDidLoad() {
    super.windowDidLoad()

    if self.contentViewController != nil {
        var vc = self.contentViewController! as NSSplitViewController
        var innerSplitView = vc.splitViewItems[0] as NSSplitViewItem
        var innerSplitViewController = innerSplitView.viewController as NSSplitViewController
        var layerCanvasSplitViewItem = innerSplitViewController.splitViewItems[1] as NSSplitViewItem
        self.layerCanvasViewController = layerCanvasSplitViewItem.viewController as LayerCanvasViewController
    }
}

Which gets me the view controller (which controls the view you see outlined in red below) and sets a local property in the window view controller.

enter image description here

So now, I can forward the toolbar button or menu item events directly in the document class which is in the responder chain and therefore receives the actions I setup in the menu and toolbar items. Like this:

class LayerDocument: NSDocument {

    @IBAction func addLayer(sender:AnyObject) {
        var windowController = self.windowControllers[0] as MainWindowController
        windowController.layerCanvasViewController.addLayer()
    }

    // ... etc.
}

Since the LayerCanvasViewController was set as a property of the main window controller when it got loaded, I can just access it and call the methods I need.

like image 145
Matt Long Avatar answered Oct 07 '22 12:10

Matt Long


For the action to find your view controllers, you need to implement -supplementalTargetForAction:sender: in your window and view controllers.

You could list all child controllers potentially interested in the action, or use a generic implementation:

- (id)supplementalTargetForAction:(SEL)action sender:(id)sender
{
    id target = [super supplementalTargetForAction:action sender:sender];

    if (target != nil) {
        return target;
    }

    for (NSViewController *childViewController in self.childViewControllers) {
        target = [NSApp targetForAction:action to:childViewController from:sender];

        if (![target respondsToSelector:action]) {
            target = [target supplementalTargetForAction:action sender:sender];
        }

        if ([target respondsToSelector:action]) {
            return target;
        }
    }

    return nil;
}
like image 26
Pierre Bernard Avatar answered Oct 07 '22 12:10

Pierre Bernard