The issue at hand is the responder chain. According to Apple:
In addition, in macOS 10.10 and later, a view controller participates in the responder chain. NSViewController
If I use a standard segue in a macOS storyboard, the destination view controller behaves as expected — focus, key equivalents, responder chain, etc.
However, if I use a custom segue, the destination view controller does not behave as expected :( Specifically, key equivalents mysteriously fail to work — However, in testing, key equivalents on buttons from the source view controller continue to work (which isn't helpful/desired)
On macOS 10.12, I'm using the following custom segue… What am I doing wrong?
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)
}
}
From the way I understand it, your question has four parts:
I have the following example functioning correctly with your custom segue class, a key equivalents example, a responder chain example, and a focus example (assuming you mean TextField focus).
As you can see in the gif below, you can click the Segue button to transition using your PushSegue
segue. Then click the Dismiss button to go back. The right and left arrow are the key equivalents for the Segue and Dismiss buttons respectively. The responder chain successfully passes data from the SheetController
(red screen) to the MainViewController
(gray screen). Also, the textFields properly get focused when segueing. I'll explain each feature in further detail.
I took your PushAnimator
subclass that you posted and just added a red background to the view so that I could see it visually. I did this by adding the following line within the animatePresentation
method of your PushAnimator
class:
viewController.view.layer?.backgroundColor = CGColor.init(red: 1, green: 0, blue: 0, alpha: 1)
The Segue button is assigned the keyEquivalent of NSRightArrowFunctionKey
and the Dismiss button is assigned NSLeftArrowFunctionKey
. You can tap the right and left arrow keys to segue between the two views. To setup the right arrow, I added the following in the MainViewController
viewDidLoad()
function:
let array = [unichar(NSRightArrowFunctionKey)]
mainViewSegueButton.keyEquivalent = String(utf16CodeUnits: array, count: 1)
To setup the left arrow, I added the following in the SheetController
viewDidLoad()
function:
let array = [unichar(NSLeftArrowFunctionKey)]
dismissButton.keyEquivalent = String(utf16CodeUnits: array, count: 1)
Since you did not describe the specific issue you're having with the responder chain, I just implemented a simple test to see if it works. I will set SheetController
's dismissButton
with a nextResponder of MainViewController
. I have the following in SheetController
's 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
To explain the above: I first set up the dismissButton
with an accessibility label so that MainViewController
can identify the button later. Setting the button's target to nil means that it will look towards its nextResponder to handle any events that it detects. I set the button's action to the dismissAction
method, which is declared within MainViewController
. And finally, I set the dismissButton
's nextResponder to MainViewController
.
Back in the MainViewController
, I set up the dismissAction
method below. It just increments a counter variable and displays that counter along with the button's accessibilityLabel (same access label that we setup earlier) to the textField. The final line dismisses the 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!)
}
For this, I assume you're referring to textField focus. In SheetController
, I have the sheetFocusTextField
focus in viewDidAppear
:
override func viewDidAppear() {
super.viewDidAppear()
sheetFocusTextField.becomeFirstResponder()
}
In MainViewController
, I have mainFocusTextField
focus in the dismissAction
method:
mainFocusTextField.becomeFirstResponder()
https://github.com/starkindustries/mac-os-segue-test