Adding hardware keyboard support to your app
At a glance
| Item |
Summary |
| Purpose |
Demonstrates global key commands, raw key presses, modifier-aware gestures, standard edit actions, and keyboard focus. |
| App architecture |
A scene-owned split controller composes file-list and canvas responders; AppDelegate publishes global commands whose selectors travel down UIKit’s responder chain. |
| Main patterns |
Responder-chain routing, optional capability protocol, weak child-to-parent delegates, diffable data sources, and in-memory model store. |
| Project style |
Programmatic UIKit target split into model, file list, root split controller, and Canvas/ feature files. |
Project structure
ProIllustratorSample/
├── AppDelegate.swift
├── SceneDelegate.swift
├── MainViewController.swift
├── FileListViewController.swift
├── GlobalKeyboardShortcutRespondable.swift
├── Model.swift
└── Canvas/
├── CanvasViewController.swift
├── AddShapeViewController.swift
├── ShapeView.swift
└── ShapeSelectionView.swift
Structure observations
- Root composition is programmatic; no storyboard owns the screen graph.
- The global-command protocol is separate because AppDelegate defines commands while active child responders implement effects.
- Raw-key and gesture movement share the canvas’s selection/movement state.
Overall architecture
flowchart LR
Scene["SceneDelegate"] --> Main["MainViewController"]
Main --> Files["FileListViewController"]
Main --> Canvas["CanvasViewController"]
Files -. weak delegate .-> Main
Main -->|selected file| Canvas
App["AppDelegate.buildMenu"] --> Commands["UIKeyCommand selectors"]
Commands -. responder chain .-> Capability["GlobalKeyboardShortcutRespondable"]
Files ..-> Capability
Canvas ..-> Capability
Files --> Store["FileStore"]
Canvas --> Model["IllustrationFile and Shape"]
Reference code
ProIllustratorSample/MainViewController.swift:11 — the split controller owns both children and translates file selection into canvas state.
class MainViewController: UISplitViewController, FileListViewControllerDelegate {
let fileListViewController = FileListViewController(style: .plain)
let canvasViewController = CanvasViewController(nibName: nil, bundle: nil)
override init(nibName: String?, bundle: Bundle?) {
super.init(nibName: nil, bundle: nil)
fileListViewController.delegate = self
viewControllers = [UINavigationController(rootViewController: fileListViewController),
UINavigationController(rootViewController: canvasViewController)]
}
}
Interpretation
UIKit’s responder chain is the command bus. The app publishes selectors globally, but the first active child that conforms and can perform the action becomes the handler; the root does not manually switch on focus.
Ownership and state
classDiagram
SceneDelegate *-- MainViewController : creates
MainViewController *-- FileListViewController : owns
MainViewController *-- CanvasViewController : owns
FileListViewController *-- FileStore : owns value
FileStore *-- IllustrationFile : stores
IllustrationFile *-- Shape : stores
CanvasViewController o-- IllustrationFile : selected reference
CanvasViewController *-- ShapeView : creates for shapes
AddShapeViewController o-- AddShapeViewControllerDelegate : weak
FileListViewController o-- FileListViewControllerDelegate : weak
Ownership evidence
ProIllustratorSample/FileListViewController.swift:15 — the list owns its store/data source but delegates selection without retaining the root.
class FileListViewController: UITableViewController {
var store = FileStore()
weak var delegate: FileListViewControllerDelegate?
var tableViewDataSource: UITableViewDiffableDataSource<Int, Int>?
}
| Owner |
Object or state |
Relationship |
Mutation authority |
| Scene delegate |
Main split controller and window |
Creates/retains root; UIKit owns scene lifetime |
Scene connection |
| Main split controller |
File-list and canvas controllers |
Creates and retains |
Root composition and selection delegate |
| File-list controller |
FileStore, diffable source, selected rows |
Owns |
List commands and callbacks |
| Canvas controller |
Selected file, shape views, selection, movement timer/direction |
Receives model reference; owns presentation/interaction state |
Canvas input handlers |
| Add-shape controller |
Shape data source |
Owns; reports choice through weak delegate |
Add-shape screen |
Class and protocol design
| Type or protocol |
Responsibility |
Depends on |
GlobalKeyboardShortcutRespondable |
Optional Objective-C selectors for global create/delete commands |
UIKit responder chain |
FileListViewControllerDelegate |
Report selected IllustrationFile |
Main split controller |
AddShapeViewControllerDelegate |
Report selected shape style |
Canvas controller |
CanvasViewController |
Handle raw presses, modifier gestures, selection, and shape rendering |
UIPress, gesture recognizer, model/view types |
FileStore |
Create, find, list, and delete illustration files |
In-memory array |
The optional @objc capability is deliberate: each responder implements only commands meaningful in its current context, and canPerformAction controls availability.
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
AddShapeViewController.delegate |
public weak |
Exposes assignment broadly but does not retain the canvas. |
Weakness prevents presentation/delegate cycles; public is wider than this internal app type requires. |
| Canvas movement/selection helpers |
private |
Only canvas input methods can hit-test, start timers, or alter internal selection mechanics. |
Keep interaction invariants local. |
addKeyCommands |
fileprivate |
Same-file code may invoke it, but other files cannot. |
File-scoped setup helper; private would also fit current use. |
shapeDataSource |
private |
Only add-shape controller configures and queries identifiers. |
Protect list implementation. |
| Models and controllers |
implicit internal |
Available throughout the app target, not exported as a library. |
Target-local collaboration. |
Reference code
ProIllustratorSample/Canvas/CanvasViewController.swift:195 — raw-key translation and movement mechanics remain private to the canvas.
private func movementDirectionForKeyPresses(_ presses: Set<UIPress>) -> UIRectEdge? {
guard let key = presses.first?.key else { return nil }
switch key.keyCode {
case .keyboardLeftArrow: return .left
case .keyboardDownArrow: return .bottom
case .keyboardUpArrow: return .top
case .keyboardRightArrow: return .right
default: return nil
}
}
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Global menu command declaration |
AppDelegate.buildMenu |
Application menu customization entry point. |
| Command capability selectors |
GlobalKeyboardShortcutRespondable |
Decouples command publication from active handler. |
| Child composition and file-selection routing |
MainViewController |
Has both sides of the split. |
| File CRUD and snapshot updates |
FileListViewController + FileStore |
List owns store and stable identifier presentation. |
| Raw presses, gestures, shape movement |
CanvasViewController |
Shares one selection and movement state machine. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Responder-chain command routing |
ProIllustratorSample/AppDelegate.swift:32 |
Global shortcuts reach the focused capable child without root dispatch code. |
| Capability protocol |
ProIllustratorSample/GlobalKeyboardShortcutRespondable.swift:19 |
Names optional command selectors shared by otherwise different screens. |
| Composite/root controller |
ProIllustratorSample/MainViewController.swift:11 |
Owns the split and mediates selection between children. |
| Weak delegate |
ProIllustratorSample/Canvas/AddShapeViewController.swift:10 |
Returns modal selection without retaining its presenter. |
| Diffable data source |
ProIllustratorSample/FileListViewController.swift:35 |
Separates stable model identifiers from table update mechanics. |
Naming conventions
...Respondable names a responder-chain capability; delegate protocols name the source controller.
- User commands use standard responder verbs:
createNewItem, deleteSelectedItem, and selectAll.
- Input-state helpers use directional verbs such as
startMoving... and stopMoving....
Canvas/ groups the view, controller, and modal needed by one feature.
Architecture takeaways
- Put global shortcuts in the application menu, but let focus and the responder chain choose the handler.
- Use
canPerformAction to express contextual command availability.
- Keep raw-key state, gesture state, and model commits in the same interaction owner.
- Use weak delegates for child-to-parent events and strong ownership from parent to child.
Source map
| Source file |
Relevant symbols |
ProIllustratorSample/AppDelegate.swift |
Global menu commands |
ProIllustratorSample/SceneDelegate.swift |
Programmatic root ownership |
ProIllustratorSample/MainViewController.swift |
Split composition and selection routing |
ProIllustratorSample/GlobalKeyboardShortcutRespondable.swift |
Optional command capability |
ProIllustratorSample/FileListViewController.swift |
Store, diffable source, and command handling |
ProIllustratorSample/Canvas/CanvasViewController.swift |
Raw keyboard and gesture interaction |
ProIllustratorSample/Model.swift |
File and shape models |