iosswiftuiviewcontrollerfirst-responderbecomefirstresponder

UIKeyCommands don't work when intermediary viewController contains two viewControllers


I believe this is a non-trivial problem related to UIKeyCommands, hierarchy of ViewControllers and/or responders.

In my iOS 9.2 app I have a class named NiceViewController that defines UIKeyCommand that results in printing something to the console.

Here's NiceViewController:

class NiceViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    let command = UIKeyCommand(input: "1", modifierFlags:UIKeyModifierFlags(), 
                   action: #selector(keyPressed), discoverabilityTitle: "nice")

    addKeyCommand(command)
  }

  func keyPressed() {
    print("works")
  }
}

When I add that NiceViewController as the only child to my main view controller all works correctly - pressing button "1" on external keyboard (physical keyboard when used in simulator) works like a charm. However when I add a second view controller to my main view controller the UIKeyCommands defined in NiceViewController stop working.

I'd love to understand why does it happen and how to ensure that having multiple child view controllers to my main view controller doesn't stop those child view controllers from handling UIKeyCommands.

Here is my main view controller:

class MainViewController: UIViewController {
  let niceViewController = NiceViewController()
  let normalViewController = UIViewController()

  override func viewDidLoad() {
    super.viewDidLoad()


    self.view.addSubview(niceViewController.view)
    self.addChildViewController(niceViewController)

    self.view.addSubview(normalViewController.view)
    // removing below line makes niceViewController accept key commands - why and how to fix it?
    self.addChildViewController(normalViewController)

  }
}    

Solution

  • I do not believe this is a problem with UIKeyCommands

    In iOS, only one View Controller at a time may manage key commands. So with your setup, you have a container view controller with a couple child view controllers. You should tell iOS that you would like NiceViewController to have control of key commands.

    Defining First Responders

    At a high level, in order to support key commands, you not only must create a UIKeyCommand and add it to the view controller, but you must also enable your view controller to become a first responder so that it is able to respond to the key commands.

    First, in any view controller that you would like to use key commands for, you should let iOS know that that controller is able to become a first responder:

    override func canBecomeFirstResponder() -> Bool {
        // some conditional logic if you wish
        return true
    }
    

    Next, you need to make sure the VC actually does become the first responder. If any VCs contain some sort of text fields that become responders (or something similar), that VC will probably become the first responder on its own, but you can always call becomeFirstResponder() on NiceViewController to make it become the first responder and, among other things, respond to key commands.

    Please see the docs for UIKeyCommand:

    The system always has the first opportunity to handle key commands. Key commands that map to known system events (such as cut, copy and paste) are automatically routed to the appropriate responder methods. For other key commands, UIKit looks for an object in the responder chain with a key command object that matches the pressed keys. If it finds such an object, it then walks the responder chain looking for the first object that implements the corresponding action method and calls the first one it finds.

    Note: While someone is interacting with the other VC and it is the first responder, NiceViewController cannot be the first responder at the same time, so you might want some key commands on the other VC as well.

    Why this isn't always necessary

    When only one VC is presented, iOS appears to assume that it will be the first responder, but when you have a container VC, iOS seems to treat the container as the first responder unless there is a child that says it is able to become the first responder.