自定义 NSStoryboardSegue 未将 NSViewController 添加到响应链

Custom NSStoryboardSegue not adding NSViewController to Responder Chain

总体目标:在 macOS 上使用自定义故事板 segue

手头的问题是响应链。 根据 Apple 的说法:

In addition, in macOS 10.10 and later, a view controller participates in the responder chain. NSViewController

如果我在 macOS 故事板中使用 标准 segue,目标视图控制器的行为符合预期——焦点、等效键、响应链等。

但是,如果我使用 自定义 seguedestination 视图控制器不会按预期运行 :( 具体来说,神秘的键等价物无法工作 — 然而,在测试中,来自 source 视图控制器的按钮上的等效键继续工作(这不是 helpful/desired)

在 macOS 10.12 上,我正在使用以下自定义转场……我做错了什么?

class PushSegue: NSStoryboardSegue {

override func perform() {
    (sourceController as AnyObject).presentViewController(destinationController as! NSViewController, animator: PushAnimator())
}
}


class PushAnimator:  NSObject, NSViewControllerPresentationAnimator  {

func animatePresentation(of viewController: NSViewController, from fromViewController: NSViewController) {
    viewController.view.wantsLayer = true
    viewController.view.frame = CGRect(x: fromViewController.view.frame.size.width, y: 0,
                                       width: fromViewController.view.frame.size.width, height: fromViewController.view.frame.size.height)
    fromViewController.addChildViewController(viewController)
    fromViewController.view.addSubview(viewController.view)

    let futureFrame = CGRect(x: 0, y: 0, width: viewController.view.frame.size.width,
                             height: viewController.view.frame.size.height)

    NSAnimationContext.runAnimationGroup({ context in
        context.duration = 0.75
        viewController.view.animator().frame = futureFrame

    }, completionHandler:nil)
}

func animateDismissal(of viewController: NSViewController, from fromViewController: NSViewController) {
    let futureFrame = CGRect(x: viewController.view.frame.size.width, y: 0,
                             width: viewController.view.frame.size.width, height: viewController.view.frame.size.height)

    NSAnimationContext.runAnimationGroup({ context in
        context.duration = 0.75
        context.completionHandler = {
            viewController.view.removeFromSuperview()
            viewController.removeFromParentViewController()
        }

        viewController.view.animator().frame = futureFrame
    }, completionHandler: nil)
    
}
}

根据我的理解,你的问题分为四个部分:

  1. 如何使用自定义转场 class
  2. KeyEquivalents
  3. 有问题
  4. 响应链
  5. 有问题
  6. 焦点问题

解决方案演示

我有以下示例与您的 自定义 segue class 一起正常运行,一个 键等价物 示例,一个 responder chain 示例,以及一个 focus 示例(假设您指的是 TextField 焦点)。

正如您在下面的 gif 中看到的,您可以单击 Segue 按钮使用您的 PushSegue segue 进行转换。然后点击 Dismiss 按钮返回。右箭头和左箭头分别是 SegueDismiss 按钮的等效键。响应者链成功地将数据从 SheetController(红色屏幕)传递到 MainViewController(灰色屏幕)。此外,textFields 在 segueing 时会正确地聚焦。我将更详细地解释每个功能。

PushSegue

我拿了你发布的 PushAnimator subclass,只是在视图中添加了红色背景,以便我可以直观地看到它。我通过在 PushAnimator class:

animatePresentation 方法中添加以下行来做到这一点
viewController.view.layer?.backgroundColor = CGColor.init(red: 1, green: 0, blue: 0, alpha: 1)

等价键

Segue 按钮分配了 NSRightArrowFunctionKey 的 keyEquivalent,Dismiss 按钮分配了 NSLeftArrowFunctionKey。您可以点击向右和向左箭头键在两个视图之间切换。为了设置右箭头,我在 MainViewController viewDidLoad() 函数中添加了以下内容:

let array = [unichar(NSRightArrowFunctionKey)]
mainViewSegueButton.keyEquivalent = String(utf16CodeUnits: array, count: 1)

为了设置左箭头,我在 SheetController viewDidLoad() 函数中添加了以下内容:

let array = [unichar(NSLeftArrowFunctionKey)]
dismissButton.keyEquivalent = String(utf16CodeUnits: array, count: 1)

响应者链

由于您没有描述您在响应链中遇到的具体问题,我只是实施了一个简单的测试以查看它是否有效。我将 SheetControllerdismissButton 设置为 MainViewController 的 nextResponder。我在 SheetControllerviewDidLoad() 中有以下内容:

// SheetController viewDidLoad() 
// Set Accessibility Label
dismissButton.setAccessibilityLabel("DismissButton")

// Set button target and action        
dismissButton.target = nil
dismissButton.action = #selector(MainViewController.dismissAction(_:))
        
// Set nextResponder
dismissButton.nextResponder = self.parent

为了解释以上内容:我首先设置了带有辅助功能标签的 dismissButton,以便 MainViewController 稍后可以识别按钮。将按钮的目标设置为 nil 意味着它将寻找 nextResponder 来处理它检测到的任何事件。我将按钮的操作设置为 dismissAction 方法,该方法在 MainViewController 中声明。最后,我将 dismissButton 的 nextResponder 设置为 MainViewController

回到 MainViewController,我在下面设置了 dismissAction 方法。它只是递增一个计数器变量并将该计数器与按钮的 accessibilityLabel(我们之前设置的相同访问标签)一起显示到 textField。最后一行取消了 SheetController.

// MainViewController
func dismissAction(_ sender: AnyObject) {
    counter += 1
    let buttonAccessLabel = (sender as! NSButton).accessibilityLabel()
    helloWorldTextField.stringValue = "Action #" + counter.description + " from: " + buttonAccessLabel!
    self.dismissViewController(self.presentedViewControllers!.first!)
}

专注

为此,我假设您指的是 textField 焦点。在 SheetController 中,我在 viewDidAppear 中有 sheetFocusTextField 焦点:

override func viewDidAppear() {
    super.viewDidAppear()
    sheetFocusTextField.becomeFirstResponder()
}

MainViewController中,我mainFocusTextField关注dismissAction方法:

mainFocusTextField.becomeFirstResponder()

Github Link

https://github.com/starkindustries/mac-os-segue-test

参考资料