Cocoa storyboard responder chain

Frames for Cocoa apps look like a great solution, since I prefer the methodology you find in iOS. However, while decomposing elements into separate view controllers is logical, I don’t understand how to transfer window control (toolbar buttons) or menu interaction until view controllers are examined. My app delegate is the first responder, and he gets the actions of the menu or toolbar, however, how can I access the view controller that I need to receive this message? You can simply go to the view manager hierarchy. If so, how do you get from the application delegate, since this is the first responder? Can you make the window controller the first responder. If so, how? In the storyboard? Where?

Since this is a high-level question, it may not matter, however, I use Swift for this project if you're interested.

+8
cocoa swift storyboard macos nswindowcontroller
source share
5 answers

I am not sure if there is a β€œ correct ” method, however I came up with a solution that I will use now. First a few details

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

  • The document that the application uses can act as the first responder and redirect any actions that I have connected.

  • The document is able to hold up the top level of the window controller, and from there I can expand the hierarchy of the view manager to access the view controller that 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 a view controller (which controls the view you see below, red below) and sets a local property in the window view controller.

enter image description here

So, now I can forward the buttons of the toolbar or the events of the menu item directly in the document class, which is in the responder chain, and therefore receives the actions that I configure in the menu and toolbar elements. 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 of the controller when it loaded, I can just access it and call the methods that I need.

+5
source share

In order to find your view controllers, you need to implement -supplementalTargetForAction: sender: in your window and view controllers.

You can specify all child controllers that are potentially interested in the action, or use a common 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; } 
+2
source share

I had the same storyboard problem, but with one window without documents. This is the iOS app port and my first OS X app. Here is my solution.

First add IBAction, as you did above, to your LayerDocument. Now go to Interface Builder. You will see that in the connection panel to the first responder in your WindowController IB has now added the submitted addLayer action. Connect your toolBarItem to this. (If you look at the connections of the first responder for any other controller, it will have the addLayer action received. I could not do anything about it. Anyway.)

Back to windowDidLoad. Add the following two lines.

 // This is the top view that is shown by the window NSView *contentView = self.window.contentView; // This forces the responder chain to start in the content view // instead of simply going up to the chain to the AppDelegate. [self.window makeFirstResponder: contentView]; 

That should do it. Now, when you click on toolbarItem, it jumps directly to your action.

+1
source share

I myself struggled with this issue.

I think the β€œright” answer is to rely on a chain of defendants. For example, to associate the action of a toolbar item, you can select the first responder of the root window. And then show the attribute inspector. In the attribute inspector add your own action (see photo).

Creating custom responder action

Then connect the toolbar item to this action. (Control the drag from the toolbar item to the first responder and select the action you just added.)

Finally, you can go to ViewController (+ 10.10) or another object, if it is in the responder chain, where you want to receive this event and add a handler.

Alternatively, instead of defining an action in the attribute inspector. You can simply write your IBAction in your ViewController. Then go to the toolbar item and drag the mouse over the window controller of the first responder - and select the newly added IBAction. The event will then move through the chain of responders until it is received by the view controller.

I think this is the right way to do this without introducing any additional communication between your controllers and / or forwarding the call manually.

The only problem I encountered - being new to Mac dev myself - sometimes the toolbar item is disabled after receiving the first event. Therefore, when I think this is the right approach, there are still some problems that I have come across.

But I can get the event elsewhere without any additional connection or gymnastics.

+1
source share

Since I am a very lazy person, I came up with the following solution based on Pierre Bernard version

 #include <objc/runtime.h> //----------------------------------------------------------------------------------------------------------- IMP classSwizzleMethod(Class cls, Method method, IMP newImp) { auto methodReplacer = class_replaceMethod; auto methodSetter = method_setImplementation; IMP originalImpl = methodReplacer(cls, method_getName(method), newImp, method_getTypeEncoding(method)); if (originalImpl == nil) originalImpl = methodSetter(method, newImp); return originalImpl; } // ---------------------------------------------------------------------------- @interface NSResponder (Utils) @end //------------------------------------------------------------------------------ @implementation NSResponder (Utils) //------------------------------------------------------------------------------ static IMP originalSupplementalTargetForActionSender; //------------------------------------------------------------------------------ static id newSupplementalTargetForActionSenderImp(id self, SEL _cmd, SEL action, id sender) { assert([NSStringFromSelector(_cmd) isEqualToString:@"supplementalTargetForAction:sender:"]); if ([self isKindOfClass:[NSWindowController class]] || [self isKindOfClass:[NSViewController class]]) { id target = ((id(*)(id, SEL, SEL, id)) originalSupplementalTargetForActionSender)(self, _cmd, action, sender); if (target != nil) return target; id childViewControllers = nil; if ([self isKindOfClass:[NSWindowController class]]) childViewControllers = [[(NSWindowController*) self contentViewController] childViewControllers]; if ([self isKindOfClass:[NSViewController class]]) childViewControllers = [(NSViewController*) self childViewControllers]; for (NSViewController *childViewController in childViewControllers) { target = [NSApp targetForAction:action to:childViewController from:sender]; if (NO == [target respondsToSelector:action]) target = [target supplementalTargetForAction:action sender:sender]; if ([target respondsToSelector:action]) return target; } } return nil; } // ---------------------------------------------------------------------------- + (void) load { Method m = nil; m = class_getInstanceMethod([NSResponder class], NSSelectorFromString(@"supplementalTargetForAction:sender:")); originalSupplementalTargetForActionSender = classSwizzleMethod([self class], m, (IMP)newSupplementalTargetForActionSenderImp); } // ---------------------------------------------------------------------------- @end //------------------------------------------------------------------------------ 

Thus, you do not need to add the forwarding code to the window controller and all view controllers (although subclassing will make it a little easier), the magic happens automatically if you have a viewcontroller for the contents of the window contents.

Swizzling is always a little dangerous, so this is far from an ideal solution, but I tried it with a very complex view / viewcontroller hierarchy using container views, worked great.

0
source share

All Articles