HardwareKeyboardInputView.swift (6755B)
1 import SwiftUI 2 import UIKit 3 4 struct HardwareKeyboardEvent { 5 let keyCode: UIKeyboardHIDUsage 6 let charactersIgnoringModifiers: String 7 let modifierFlags: UIKeyModifierFlags 8 } 9 10 struct HardwareKeyboardInputView: UIViewRepresentable { 11 var onPress: (HardwareKeyboardEvent) -> Bool 12 /// When false, the capture view relinquishes first responder so another 13 /// view (the rebus text field) can own input and raise the system keyboard. 14 var isActive: Bool = true 15 16 func makeUIView(context: Context) -> KeyCaptureView { 17 let view = KeyCaptureView() 18 view.onPress = onPress 19 return view 20 } 21 22 func updateUIView(_ uiView: KeyCaptureView, context: Context) { 23 uiView.onPress = onPress 24 if isActive { 25 uiView.ensureFirstResponder() 26 } else if uiView.isFirstResponder { 27 uiView.resignFirstResponder() 28 } 29 } 30 31 final class KeyCaptureView: UIView { 32 var onPress: ((HardwareKeyboardEvent) -> Bool)? 33 34 override var canBecomeFirstResponder: Bool { true } 35 36 override var keyCommands: [UIKeyCommand]? { 37 let letters = "abcdefghijklmnopqrstuvwxyz".map { 38 UIKeyCommand( 39 input: String($0), 40 modifierFlags: [], 41 action: #selector(handleKeyCommand(_:)) 42 ) 43 } 44 45 let digits = "0123456789".map { 46 UIKeyCommand( 47 input: String($0), 48 modifierFlags: [], 49 action: #selector(handleKeyCommand(_:)) 50 ) 51 } 52 53 // Undo/redo (⌘Z, ⇧⌘Z) are vended by the app menu (PuzzleCommands) so 54 // they show in the hold-⌘ shortcut overlay; leaving them out here 55 // lets those presses bubble up to that menu command. 56 return letters + digits + [ 57 UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))), 58 UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))), 59 UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: .command, action: #selector(handleKeyCommand(_:))), 60 UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: .command, action: #selector(handleKeyCommand(_:))), 61 UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))), 62 UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))), 63 UIKeyCommand(input: "\u{8}", modifierFlags: [], action: #selector(handleKeyCommand(_:))), 64 UIKeyCommand(input: "\u{7F}", modifierFlags: [], action: #selector(handleKeyCommand(_:))), 65 UIKeyCommand(input: "\t", modifierFlags: [], action: #selector(handleKeyCommand(_:))), 66 UIKeyCommand(input: "\t", modifierFlags: .shift, action: #selector(handleKeyCommand(_:))), 67 UIKeyCommand(input: " ", modifierFlags: [], action: #selector(handleKeyCommand(_:))), 68 UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(handleKeyCommand(_:))), 69 UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(handleKeyCommand(_:))) 70 ] 71 } 72 73 override func didMoveToWindow() { 74 super.didMoveToWindow() 75 ensureFirstResponder() 76 } 77 78 func ensureFirstResponder() { 79 guard window != nil, !isFirstResponder else { return } 80 Task { @MainActor in 81 self.becomeFirstResponder() 82 } 83 } 84 85 override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) { 86 var unhandled: [UIPress] = [] 87 88 for press in presses { 89 guard let key = press.key else { 90 unhandled.append(press) 91 continue 92 } 93 94 let event = HardwareKeyboardEvent( 95 keyCode: key.keyCode, 96 charactersIgnoringModifiers: key.charactersIgnoringModifiers, 97 modifierFlags: key.modifierFlags 98 ) 99 100 if onPress?(event) != true { 101 unhandled.append(press) 102 } 103 } 104 105 if !unhandled.isEmpty { 106 super.pressesBegan(Set(unhandled), with: event) 107 } 108 } 109 110 @objc private func handleKeyCommand(_ command: UIKeyCommand) { 111 guard let event = HardwareKeyboardEvent( 112 characters: command.input ?? "", 113 modifierFlags: command.modifierFlags 114 ) else { return } 115 _ = onPress?(event) 116 } 117 } 118 } 119 120 private extension HardwareKeyboardEvent { 121 init?(characters: String, modifierFlags: UIKeyModifierFlags) { 122 let keyCode: UIKeyboardHIDUsage 123 124 switch characters { 125 case UIKeyCommand.inputLeftArrow: 126 keyCode = .keyboardLeftArrow 127 case UIKeyCommand.inputRightArrow: 128 keyCode = .keyboardRightArrow 129 case UIKeyCommand.inputUpArrow: 130 keyCode = .keyboardUpArrow 131 case UIKeyCommand.inputDownArrow: 132 keyCode = .keyboardDownArrow 133 case UIKeyCommand.inputEscape: 134 keyCode = .keyboardEscape 135 case "\u{8}": 136 keyCode = .keyboardDeleteOrBackspace 137 case "\u{7F}": 138 keyCode = .keyboardDeleteForward 139 case "\t": 140 keyCode = .keyboardTab 141 case " ": 142 keyCode = .keyboardSpacebar 143 case "\r": 144 keyCode = .keyboardReturnOrEnter 145 case "a"..."z", "A"..."Z": 146 guard let scalar = characters.uppercased().unicodeScalars.first, 147 let code = UIKeyboardHIDUsage(rawValue: Int(scalar.value - 65) + UIKeyboardHIDUsage.keyboardA.rawValue) else { 148 return nil 149 } 150 keyCode = code 151 case "1"..."9": 152 // HID usages run keyboard1…keyboard9 contiguously (keyboard0 sits 153 // after, handled separately below). 154 guard let digit = characters.first?.wholeNumberValue, 155 let code = UIKeyboardHIDUsage(rawValue: UIKeyboardHIDUsage.keyboard1.rawValue + (digit - 1)) else { 156 return nil 157 } 158 keyCode = code 159 case "0": 160 keyCode = .keyboard0 161 default: 162 return nil 163 } 164 165 self.init( 166 keyCode: keyCode, 167 charactersIgnoringModifiers: characters, 168 modifierFlags: modifierFlags 169 ) 170 } 171 }