简体   繁体   English

可可故事板响应者链

[英]Cocoa Storyboard Responder Chain

Storyboards for Cocoa apps seems like a great solution as I prefer the methodology you find in iOS. Cocoa应用程序的故事板似乎是一个很好的解决方案,因为我更喜欢你在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. 由于这是一个高级别的问题,因此,如果您想知道,我正在使用Swift进行此项目。

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: 所以,在窗口控制器上的windowDidLoad中,我这样做:

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. 这让我获得了视图控制器(它控制下面用红色标出的视图)并在窗口视图控制器中设置一个本地属性。

在此输入图像描述

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. 由于LayerCanvasViewController在加载时被设置为主窗口控制器的属性,我可以访问它并调用我需要的方法。

For the action to find your view controllers, you need to implement -supplementalTargetForAction:sender: in your window and view controllers. 要查找视图控制器的操作,您需要在窗口中实现-supplementalTargetForAction:sender:并查看控制器。

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;
}

I had the same Storyboard problem but with a single window app with no Documents. 我有相同的故事板问题,但有一个没有文档的窗口应用程序。 It's a port of an iOS app, and my first OS X app. 它是iOS应用程序的一个端口,也是我的第一个OS X应用程序。 Here's my solution. 这是我的解决方案。

First add an IBAction as you did above in your LayerDocument. 首先在LayerDocument中添加一个IBAction。 Now go to Interface Builder. 现在转到Interface Builder。 You'll see that in the connections panel to First Responder in your WindowController, IB has now added a Sent Action of addLayer. 您将看到在WindowController的First Responder的连接面板中,IB现在添加了addLayer的Sent Action。 Connect your toolBarItem to this. 将toolBarItem连接到此。 (If you look at First Responder connections for any other controller, it will have a Received Action of addLayer. I couldn't do anything with this. Whatever.) (如果您查看任何其他控制器的First Responder连接,它将具有addLayer的Received Action。我无法对此做任何事情。无论如何。)

Back to windowDidLoad. 回到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 the toolbarItem it will go directly to your action. 现在,当您单击toolbarItem时,它将直接转到您的操作。

I've been struggling with this question myself. 我自己一直在努力解决这个问题。

I think the 'correct' answer is to lean on the responder chain. 我认为“正确”的答案是依靠响应者链。 For example, to connect a tool bar item action, you can select the root window controller's first responder. 例如,要连接工具栏项操作,可以选择根窗口控制器的第一响应者。 And then show the attributes inspector. 然后显示属性检查器。 In the attributes inspector, add your custom action (see photo). 在属性检查器中,添加自定义操作(请参见照片)。

创建自定义响应程序操作

Then connect your toolbar item to that action. 然后将工具栏项连接到该操作。 (Control drag from your Toolbar item to the first responder and select the action you just added.) (控制从工具栏项拖动到第一个响应者并选择刚刚添加的操作。)

Finally, you can then go to the ViewController (+ 10.10) or other object, so long as its in the responder chain, where you want to receive this event and add the handler. 最后,您可以转到ViewController(+ 10.10)或其他对象,只要它在响应程序链中,您希望接收此事件并添加处理程序。

Alternatively, instead of defining the action in the attributes inspector. 或者,而不是在属性检查器中定义操作。 You can simply write your IBAction in your ViewController. 您只需在ViewController中编写IBAction即可。 Then, go to the toolbar item, and control drag to the window controller's first responder -- and select the IBAction you just added. 然后,转到工具栏项,并控制拖动到窗口控制器的第一响应者 - 并选择刚刚添加的IBAction。 The event will then travel thru the responder chain until received by the view controller. 然后,事件将通过响应器链传输,直到视图控制器接收为止。

I think this is the correct way to do this without introducing any additional coupling between your controllers and/or manually forwarding the call. 我认为这是正确的方法,无需在控制器之间引入任何额外的耦合和/或手动转发呼叫。

The only challenge I've run into -- being new to Mac dev myself -- is sometimes the Toolbar item disabled itself after receiving the first event. 我遇到的唯一挑战 - 自己是Mac dev的新手 - 有时候工具栏项在收到第一个事件后自行禁用。 So, while I think this is the correct approach, there are still some issues I've run into myself. 所以,虽然我认为这是正确的方法,但我仍然遇到了一些问题。

But I am able to receive the event in another location without any additional coupling or gymnastics. 但我能够在另一个地方接收活动,而无需任何额外的耦合或体操。

As i'm a very lazy person i came up with the following solution based on Pierre Bernard 's 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
//------------------------------------------------------------------------------

This way you do not have to add the forwarder code to the window controller and all the viewcontrollers (although subclassing would make that a bit easier), the magic happens automatically if you have a viewcontroller for the window contentview. 这样您就不必将转发器代码添加到窗口控制器和所有视图控制器(尽管子类化会使这更容易),如果您有窗口内容视图的视图控制器,则会自动发生魔术。

Swizzling always a bit dangerous so it is far not a perfect solution, but I've tried it with a very complex view/viewcontroller hierarchy that using container views, worked fine. Swizzling总是有点危险所以它远远不是一个完美的解决方案,但我已经尝试了一个非常复杂的视图/ viewcontroller层次结构,使用容器视图,工作正常。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM