I'm working on adding keyboard shortcuts on my application. There is a view controller that presents another controller:
class ViewController: UIViewController {
override var canBecomeFirstResponder: Bool { true }
override func viewDidLoad() {
super.viewDidLoad()
addKeyCommand(UIKeyCommand(
input: "M",
modifierFlags: .command,
action: #selector(ViewController.handleKeyCommand),
discoverabilityTitle: "Command from the container view"
))
}
@objc func handleKeyCommand() {
present(ModalViewController(), animated: true)
}
override func canPerformAction(
_ action: Selector, withSender sender: Any?
) -> Bool {
if action == #selector(ViewController.handleKeyCommand) {
return isFirstResponder
}
return super.canPerformAction(action, withSender: sender)
}
}
class ModalViewController: UIViewController {
override var canBecomeFirstResponder: Bool { true }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
addKeyCommand(UIKeyCommand(
input: "D",
modifierFlags: .command,
action: #selector(ModalViewController.handleKeyCommand),
discoverabilityTitle: "Command from the modal view"
))
if !becomeFirstResponder() {
print("⚠️ modal did not become first responder")
}
}
@objc func handleKeyCommand() {
dismiss(animated: true)
}
}
Both define shortcuts. When the modal view controller is presented, the Discoverability popup includes shortcuts for both presenting and presented view controller. Intuitively, only the modal view controller shortcuts should be included (we are not supposed to be able to interact with the presenting view controller, right?)
I can fix this by overriding the presenting view controller's keyCommands property, but is this a good idea?
In general, what is the reason behind this behavior? Is this a bug or a feature?
UPDATE: Added the canPerformAction(_:sender:) to the presenting view controller (as suggested by @buzzert), but the problem persists.
This is happening because the presenting view controller (
ViewController) is yourModalViewController'snextResponderin the responder chain.This is because the OS needs some way to trace from the view controller that's currently presented on screen all the way back up to the application.
If your presenting view controller only has commands that make sense when it is first responder, the easiest way to resolve this is by simply overriding
canPerformAction(_:)onViewController, and returnfalseif it is not first responder.For example,
Otherwise, if you want more control over the
nextResponderin the responder chain, you can also override thenextRespondergetter to "skip" your presenting view controller. This is not recommended though, but serves as an illustration of how it works.