自定义 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,目标视图控制器的行为符合预期——焦点、等效键、响应链等。
但是,如果我使用 自定义 segue,destination 视图控制器不会按预期运行 :( 具体来说,神秘的键等价物无法工作 — 然而,在测试中,来自 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)
}
}
根据我的理解,你的问题分为四个部分:
- 如何使用自定义转场 class
- KeyEquivalents
有问题
- 响应链
有问题
- 焦点问题
解决方案演示
我有以下示例与您的 自定义 segue class 一起正常运行,一个 键等价物 示例,一个 responder chain 示例,以及一个 focus 示例(假设您指的是 TextField 焦点)。
正如您在下面的 gif 中看到的,您可以单击 Segue 按钮使用您的 PushSegue
segue 进行转换。然后点击 Dismiss 按钮返回。右箭头和左箭头分别是 Segue 和 Dismiss 按钮的等效键。响应者链成功地将数据从 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)
响应者链
由于您没有描述您在响应链中遇到的具体问题,我只是实施了一个简单的测试以查看它是否有效。我将 SheetController
的 dismissButton
设置为 MainViewController
的 nextResponder。我在 SheetController
的 viewDidLoad()
中有以下内容:
// 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
参考资料
- Apple API Reference: keyEquivalent
- Apple API Reference: Event Architecture
- Whosebug: What is the formal definition of FirstResponder
- Apple API Reference: Responder Object
总体目标:在 macOS 上使用自定义故事板 segue
手头的问题是响应链。 根据 Apple 的说法:
In addition, in macOS 10.10 and later, a view controller participates in the responder chain. NSViewController
如果我在 macOS 故事板中使用 标准 segue,目标视图控制器的行为符合预期——焦点、等效键、响应链等。
但是,如果我使用 自定义 segue,destination 视图控制器不会按预期运行 :( 具体来说,神秘的键等价物无法工作 — 然而,在测试中,来自 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)
}
}
根据我的理解,你的问题分为四个部分:
- 如何使用自定义转场 class
- KeyEquivalents 有问题
- 响应链 有问题
- 焦点问题
解决方案演示
我有以下示例与您的 自定义 segue class 一起正常运行,一个 键等价物 示例,一个 responder chain 示例,以及一个 focus 示例(假设您指的是 TextField 焦点)。
正如您在下面的 gif 中看到的,您可以单击 Segue 按钮使用您的 PushSegue
segue 进行转换。然后点击 Dismiss 按钮返回。右箭头和左箭头分别是 Segue 和 Dismiss 按钮的等效键。响应者链成功地将数据从 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)
响应者链
由于您没有描述您在响应链中遇到的具体问题,我只是实施了一个简单的测试以查看它是否有效。我将 SheetController
的 dismissButton
设置为 MainViewController
的 nextResponder。我在 SheetController
的 viewDidLoad()
中有以下内容:
// 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
参考资料
- Apple API Reference: keyEquivalent
- Apple API Reference: Event Architecture
- Whosebug: What is the formal definition of FirstResponder
- Apple API Reference: Responder Object