Cocoa Storyboard响应链

9

对于Cocoa应用程序,故事板似乎是一个很好的解决方案,因为我更喜欢在iOS中找到的方法论。然而,虽然将事物分解为单独的视图控制器在逻辑上有很多意义,但我不清楚如何将窗口控制(工具栏按钮)或菜单交互传递给关心的视图控制器。我的应用程序委托是第一响应者,它接收菜单或工具栏操作,但是,我如何访问需要将该消息传递给的视图控制器?您可以直接深入到视图控制器层次结构中吗?如果可以,从应用程序委托如何进入那里,因为它是第一响应者?您可以使窗口控制器成为第一响应者。如果可以,如何实现?在故事板中?哪里?

由于这是一个高级问题,可能并不重要,但是如果您想知道,我正在使用Swift进行此项目。

5个回答

6
我不确定是否有一种“适当”的方法来解决这个问题,但是我已经想出了一个解决方案,现在将使用它。首先是一些细节:
- 我的应用程序是基于文档的应用程序,因此每个窗口都有一个文档实例。 - 应用程序使用的文档可以充当第一响应者并转发我连接的任何操作。 - 该文档能够获得最顶层的窗口控制器,从那里我能够通过视图控制器层次结构深入到我需要的视图控制器。
因此,在我的窗口控制器的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
    }
}

这使我得到了视图控制器(它控制着您在下面用红色轮廓看到的视图)并在窗口视图控制器中设置了一个本地属性。

enter image description here

所以现在,我可以直接将工具栏按钮或菜单项事件转发到文档类中,该类在响应者链中,因此会接收我在菜单和工具栏项目中设置的操作。就像这样:

class LayerDocument: NSDocument {

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

    // ... etc.
}

由于LayerCanvasViewController在加载时被设置为主窗口控制器的属性,因此我可以直接访问它并调用所需的方法。


2

为了找到你的视图控制器执行操作,你需要在你的窗口和视图控制器中实现-supplementalTargetForAction:sender:

你可以列出所有可能对该操作感兴趣的子控制器,或使用通用实现:

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

1
我遇到了与单窗口应用程序和没有文档的Storyboard问题。这是一个iOS应用程序的端口,也是我的第一个OS X应用程序。以下是我的解决方案。
首先,在LayerDocument中添加一个IBAction,就像你在上面做的那样。现在打开Interface Builder。您会发现在WindowController的连接面板中,IB现在已经添加了一个名为addLayer的Sent Action。将您的toolBarItem连接到此处。(如果您查看任何其他控制器的First Responder连接,它将具有名为addLayer的Received Action。我无法对此进行任何操作。)
回到windowDidLoad。添加以下两行代码。
//  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];

这样就可以了。现在当你点击工具栏项时,它会直接转到你的操作。


2
这仅适用于第一次。当第一个响应者更改为不具有addLayer操作的对象时,该操作将不会被调用。例如,当您有一个SplitViewController并选择其中一个项目时,就是这种情况。其他项不再在所选项目的链中。 - JanApotheker

1
我自己也苦恼于这个问题。我认为“正确”的答案是依赖响应者链。例如,想要连接一个工具栏项的操作,您可以选择根窗口控制器的第一响应者。然后显示属性检查器,在属性检查器中添加您的自定义动作(如图所示)。

Creating custom responder action

然后将您的工具栏项连接到该操作。(从工具栏项中控制拖动到第一个响应者并选择刚添加的操作。)
最后,您可以转到视图控制器(+10.10)或其他对象,只要它在响应链中,就可以接收此事件并添加处理程序。
或者,您可以不在属性检查器中定义操作。您可以在ViewController中简单地编写您的IBAction。然后,转到工具栏项,并控制拖动到窗口控制器的第一个响应者 - 并选择刚添加的IBAction。然后,事件将通过响应者链传递,直到被视图控制器接收。
我认为这是在不引入任何额外耦合或手动转发调用的情况下完成此操作的正确方法。
唯一遇到的挑战是——作为自己的Mac开发新手——有时工具栏项在接收到第一个事件后会自行禁用。因此,虽然我认为这是正确的方法,但我仍然遇到了一些问题。
但是,我能够在另一个位置接收到事件,而无需进行任何额外的耦合或花式操作。

0

由于我是一个非常懒惰的人,所以我基于Pierre Bernard的版本提出了以下解决方案

#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
//------------------------------------------------------------------------------

这样做的好处是您不必将转发器代码添加到窗口控制器和所有视图控制器中(尽管子类化会使这个过程变得更容易),如果您有一个用于窗口内容视图的视图控制器,那么魔法就会自动发生。

交换方法总是有一定风险的,因此它并不是完美的解决方案,但我已经尝试过使用包含容器视图的非常复杂的视图/视图控制器层次结构,效果很好。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接