The UIKeyCommand class allows you to add keyboard shortcuts to your app. Although they might seem useful for iPad only, there are reasons to add this to your iPhone app as well. They’re easy to set up and they work in the simulator!
After I wrote Shortcuts essentials in Xcode to speed up your workflow and Useful less known Xcode tips to improve your workflow this is another great opportunity to change your workflow and speed it up.
Getting used to this can speed up your development cycle by a lot.
How to implement UIKeyCommand
Adding a UIKeyCommand to your custom responder class is easy. Every UIResponder
class comes with a keyCommands
collection variable. By overriding this property you can provide the UIKeyCommands you want to enable. Key command sequences are generated only for devices with an attached hardware keyboard.
override var keyCommands: [UIKeyCommand]? {
return [UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: .shift, action: #selector(dismiss), discoverabilityTitle: "Close modal view")]
}
In this example, we’ve created a keyboard shortcut that will dismiss the view once you tap SHIFT + ESC
.
To make this key command actually work, we need to tell the responder chain that our view can become the first responder.
override var canBecomeFirstResponder: Bool {
return true
}
It’s good to know that the system is always checked first for the pressed key commands. The system will stop your shortcut from working if it has a shortcut assigned to the combination. For other key commands, the responder chain is used to find the first object that implements a key command for the pressed keys.
UIKeyCommand options for set up
The UIKeyCommand allows you to set up a combination of keys. Its parameters work as follows:
Input
This is the key that the user must press to make the key command work. A list of strings for special keys allows you to catch input for:
- Arrow up
- Arrow down
- Arrow reft
- Arrow right
- Escape
Modifier Flags
This is the place to define the key combinations and with that, the keys the user also has to press. Examples are the command, option and shift keys. For a full list, you can check out UIKeyModifierFlags.
Action
This is simply the selector to execute once the key combination is pressed.
Discoverability title
Although this property is deprecated as of iOS 13, it is required up until. This String value simply explains the purpose of the key command to the user. A dialog showing all keyboard shortcuts is available upon holding command on the iPad.
Speeding up your workflow with key commands
Now that you know how to set up some key commands it is time to speed up your workflow. By defining key commands for your common views you can easily navigate through your app without using the mouse. Getting used to this can speed up your development cycle by a lot.
As testing in the simulator only allows shortcuts which aren’t already used by the simulator itself, you often need to make key combinations using shift
while command
sometimes feels more natural. By always using shift
you make your keys easy accessible by just holding on shift
at all time.
Adding keyboard shortcuts to a UITabBarController
For a tab bar controller it is great to have the possibility to switch tabs using a keyboard shortcut. In this case, we’d like to switch using SHIFT + Tab Index
.
// MARK: - Keyboard Shortcuts
extension HomeTabBarController {
/// Adds keyboard shortcuts for the tabs.
/// - Shift + Tab Index for the simulator
override var keyCommands: [UIKeyCommand]? {
return tabBar.items?.enumerated().map { (index, item) -> UIKeyCommand in
return UIKeyCommand(input: "\(index + 1)", modifierFlags: .shift, action: #selector(selectTab), discoverabilityTitle: item.title ?? "Tab \(index + 1)")
}
}
@objc private func selectTab(sender: UIKeyCommand) {
guard let input = sender.input, let newIndex = Int(input), newIndex >= 1 && newIndex <= (tabBar.items?.count ?? 0) else { return }
selectedIndex = newIndex - 1
}
override var canBecomeFirstResponder: Bool {
return true
}
}
Adding a UIKeyCommand to a UINavigationController
Navigating back from a page feels natural if we could use the back arrow just like in many browsers. Even Xcode provides us this option by using CTRL + COMMAND + Back arrow
.
Adding this to our custom navigation controller class takes the following lines of code:
// MARK: - Keyboard Shortcuts
extension NavigationController {
/*
Adds keyboard shortcuts to navigate back in a navigation controller.
- Shift + left arrow on the simulator
*/
override public var keyCommands: [UIKeyCommand]? {
guard viewControllers.count > 1 else { return [] }
return [UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: .shift, action: #selector(backCommand), discoverabilityTitle: "Back")]
}
@objc private func backCommand() {
popViewController(animated: true)
}
override var canBecomeFirstResponder: Bool {
return true
}
}
Controlling a UICollectionView using keyboard shortcuts
From all those examples, this is the hardest one. We need a way to keep track of the focussed cell and we also need to highlight a cell upon focussing. This almost feels like replicating the focus engine of tvOS.
Note: This code only works for a UICollectionView, but is easily convertible for a UITableView.
We’re first creating a custom CollectionViewKeyCommandsController
to make this code reusable for all our UICollectionView
classes in our app.
This class takes reference to the focussed indexPath and changes it accordingly based on the data available in the UICollectionView
. The methods are best to connect to the following key combinations:
SHIFT + Escape
: Reset focusSHIFT + Arrow down
: Go to the next cellSHIFT + Arrow up
: Go to the previous cellSHIFT + Enter
: Select the currently focussed cell
The code for this looks as followed:
/// A commandscontroller to enable keyboard shortcuts in our app with a `UICollectionView`.
final class CollectionViewKeyCommandsController {
private let collectionView: UICollectionView
private var focussedIndexPath: IndexPath? {
didSet {
guard let focussedIndexPath = focussedIndexPath, let focussedCell = collectionView.cellForItem(at: focussedIndexPath) else { return }
UIView.animate(withDuration: 0.2, animations: {
focussedCell.alpha = 0.5
}, completion: { _ in
UIView.animate(withDuration: 0.2, animations: {
focussedCell.alpha = 1.0
})
})
}
}
init(collectionView: UICollectionView) {
self.collectionView = collectionView
}
func escapeKeyTapped() {
focussedIndexPath = nil
}
func enterKeyTapped() {
guard let focussedIndexPath = focussedIndexPath else { return }
collectionView.delegate?.collectionView?(collectionView, didSelectItemAt: focussedIndexPath)
}
func nextKeyTapped() {
guard let focussedIndexPath = focussedIndexPath else {
self.focussedIndexPath = firstIndexPath
return
}
let numberOfItems = collectionView.numberOfItems(inSection: 0)
let focussedItem = focussedIndexPath.item
guard focussedItem != (numberOfItems - 1) else {
self.focussedIndexPath = firstIndexPath
return
}
self.focussedIndexPath = IndexPath(item: focussedItem + 1, section: 0)
}
func previousKeyTapped() {
guard let focussedIndexPath = focussedIndexPath else {
self.focussedIndexPath = lastIndexPath
return
}
let focussedItem = focussedIndexPath.item
guard focussedItem > 0 else {
self.focussedIndexPath = lastIndexPath
return
}
self.focussedIndexPath = IndexPath(item: focussedItem - 1, section: 0)
}
private var lastIndexPath: IndexPath {
return IndexPath(item: collectionView.numberOfItems(inSection: 0) - 1, section: 0)
}
private var firstIndexPath: IndexPath {
return IndexPath(item: 0, section: 0)
}
}
Now, we need to save an instance of this class on our view controller which contains the collection view.
/// A controller to handle all keyboard input.
fileprivate lazy var collectionViewKeyCommandsController = CollectionViewKeyCommandsController(collectionView: collectionView)
After that, we can implement the code as before to set up the key commands.
// MARK: - Keyboard Shortcuts
extension EditableCollectionViewController {
override var keyCommands: [UIKeyCommand]? {
return [
UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: .shift, action: #selector(escapeKeyTapped), discoverabilityTitle: "Reset cell focus"),
UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: .shift, action: #selector(nextKeyTapped), discoverabilityTitle: "Next item"),
UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: .shift, action: #selector(previousKeyTapped), discoverabilityTitle: "Previous item"),
UIKeyCommand(input: "\r", modifierFlags: .shift, action: #selector(selectKeyTapped), discoverabilityTitle: "Select item"),
UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: .shift, action: #selector(selectKeyTapped), discoverabilityTitle: "Select item")
]
}
override var canBecomeFirstResponder: Bool {
return true
}
@objc func escapeKeyTapped() {
collectionViewKeyCommandsController.escapeKeyTapped()
}
@objc func selectKeyTapped() {
collectionViewKeyCommandsController.enterKeyTapped()
}
@objc func nextKeyTapped() {
collectionViewKeyCommandsController.nextKeyTapped()
}
@objc func previousKeyTapped() {
collectionViewKeyCommandsController.previousKeyTapped()
}
}
Although this still results in quite some duplicate code once you want to add support to multiple views, it is the most efficient way possible. Trying to make CollectionViewKeyCommandsController
conform to the UIResponder class didn’t work out as explained in the following chapter.
Creating a custom UIResponder class
It turns out that it’s hard to add a custom responder into the responder chain. It would’ve been as easy as passing in your custom instance as the nextResponder
by overriding the same named property. However, the responder chain does not seem to like this and therefore forces us to define the key commands within the views themselves.
Conclusion
The UIKeyCommand class allows us to create a win-win situation. We’re adding keyboard support to our (future) iPad app and we also enable ourselves to navigate through our app on the simulator.
With the above code examples, it should be very easy to add basic shortcuts to your app. Obviously, you can create some for yourself as well based on your needs. Some ideas:
- Refresh using
SHIFT + R
- Enable debug mode using
SHIFT + COMMAND + D
The possibilities are endless. Good luck!
Thanks.