【问题标题】:Cocoa Storyboard Responder Chain可可故事板响应者链
【发布时间】:2014-12-31 22:02:22
【问题描述】:

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

由于这是一个高级问题,它可能并不重要,但是,如果您想知道的话,我会在这个项目中使用 Swift。

【问题讨论】:

    标签: macos cocoa swift storyboard nswindowcontroller


    【解决方案1】:

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

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

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

    • 该文档能够获取顶级窗口控制器,并且我可以从那里深入查看视图控制器层次结构以找到我需要的视图控制器。

    所以,在窗口控制器上的 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
        }
    }
    

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

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

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

      【讨论】:

        【解决方案3】:

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

        首先像上面那样在 LayerDocument 中添加一个 IBAction。现在转到界面生成器。您会看到,在 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];
        

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

        【讨论】:

        • 这仅适用于第一次。当第一响应者更改为链中没有addLayer 操作的对象时,将不会调用该操作。例如,当您有一个 SplitViewController 并选择了其中一个项目时,就是这种情况。其他项目不再在所选项目的链中。
        【解决方案4】:

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

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

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

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

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

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

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

        但我无需任何额外的耦合或体操就可以在其他地点接收活动。

        【讨论】:

          【解决方案5】:

          由于我是一个非常懒惰的人,我根据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
          //------------------------------------------------------------------------------
          

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

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2016-11-30
            • 1970-01-01
            • 2020-05-15
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多