可可故事板响应者链

时间:2014-11-04 20:57:32

标签: macos cocoa swift storyboard nswindowcontroller

Cocoa应用程序的故事板似乎是一个很好的解决方案,因为我更喜欢你在iOS中找到的方法。然而,虽然将事物分解为单独的视图控制器具有很大的逻辑意义,但我不清楚如何将窗口控件(工具栏按钮)或菜单交互传递给关心的视图控制器。我的应用程序委托是第一个响应者,它接收菜单或工具栏操作,但是,如何访问我需要获取该消息的视图控制器?您是否可以深入查看视图控制器层次结构。如果是这样,你是如何从应用代表那里到达那里的,因为它是第一响应者?你能否将窗口控制器作为第一响应者。如果是这样,怎么样?在故事板?在哪里?

因为这是一个高级别的问题,所以可能无关紧要,但是,如果你想知道我正在使用Swift进行这个项目。

5 个答案:

答案 0 :(得分:5)

我不确定是否有“正确的”方法来解决这个问题,但是,我已经提出了一个我现在将使用的解决方案。首先是几个细节

  • 我的应用程序是基于文档的应用程序,因此每个窗口都有一个文档实例。

  • 应用程序使用的文档可以充当第一响应者并转发我已连接的任何操作

  • 该文档能够保存顶级窗口控制器,从那里我可以向下钻取视图控制器层次结构,以获得我需要的视图控制器。

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

答案 1 :(得分: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;
}

答案 2 :(得分:1)

我有相同的Storyboard问题,但有一个没有文档的窗口应用程序。它是iOS应用程序的一个端口,也是我的第一个OS X应用程序。这是我的解决方案。

首先在LayerDocument中添加如上所述的IBAction。现在转到Interface Builder。您将看到在WindowController的First Responder的连接面板中,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];

应该这样做。现在,当您单击toolbarItem时,它将直接进入您的操作。

答案 3 :(得分:1)

我自己一直在努力解决这个问题。

我认为'正确'答案是依靠响应者链。例如,要连接工具栏项操作,可以选择根窗口控制器的第一响应者。然后显示属性检查器。在属性检查器中,添加自定义操作(参见照片)。

Creating custom responder action

然后将工具栏项目连接到该操作。 (控制从工具栏项拖动到第一个响应者并选择刚刚添加的操作。)

最后,您可以转到ViewController(+ 10.10)或其他对象,只要它在响应程序链中,您希望接收此事件并添加处理程序。

或者,而不是在属性检查器中定义操作。您只需在ViewController中编写IBAction即可。然后,转到工具栏项,并控制拖动到窗口控制器的第一响应者 - 并选择刚刚添加的IBAction。然后,事件将通过响应器链传输,直到视图控制器接收为止。

我认为这是正确的方法,无需在控制器之间引入任何额外的耦合和/或手动转发呼叫。

我遇到的唯一挑战 - 自己是Mac开发新手 - 有时候工具栏项在收到第一个事件后自行禁用。所以,虽然我认为这是正确的方法,但我仍然遇到一些问题。

但是我可以在另一个地方接收这个活动而不需要任何额外的耦合或体操。

答案 4 :(得分: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
//------------------------------------------------------------------------------

通过这种方式,您不必将转发器代码添加到窗口控制器和所有视图控制器(尽管子类化会使这更容易),如果您有窗口内容视图的viewcontroller,则会自动发生魔术。

Swizzling总是有点危险所以它远远不是一个完美的解决方案,但我已经尝试使用非常复杂的视图/ viewcontroller层次结构,使用容器视图,工作正常。