swiftxcodemacos-sierransviewcontrollernsresponder

Custom NSStoryboardSegue not adding NSViewController to Responder Chain


Overall goal: Use a custom storyboard segue on macOS

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

Solution

  • From the way I understand it, your question has four parts:

    1. How to use a custom segue class
    2. An issue with KeyEquivalents
    3. An issue with the Responder Chain
    4. An issue with focus

    Solution Demo

    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.

    enter image description here

    PushSegue

    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)
    

    Key Equivalents

    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)
    

    Responder Chain

    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!)
    }
    

    Focus

    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()
    

    Github Link

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

    References