HardwareKeyboardInputView.swift (9264B)
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 private static let suspendCaptureNotification = Notification.Name( 12 "HardwareKeyboardInputView.suspendCapture" 13 ) 14 private static let resumeCaptureNotification = Notification.Name( 15 "HardwareKeyboardInputView.resumeCapture" 16 ) 17 @MainActor private static var isCaptureSuspended = false 18 19 var onPress: (HardwareKeyboardEvent) -> Bool 20 /// When false, the capture view relinquishes first responder so another 21 /// view (the rebus text field) can own input and raise the system keyboard. 22 var isActive: Bool = true 23 24 func makeUIView(context: Context) -> KeyCaptureView { 25 let view = KeyCaptureView() 26 view.onPress = onPress 27 view.setActive(isActive) 28 return view 29 } 30 31 func updateUIView(_ uiView: KeyCaptureView, context: Context) { 32 uiView.onPress = onPress 33 uiView.setActive(isActive) 34 } 35 36 /// Temporarily yields hardware-keyboard ownership to UIKit content 37 /// presented above the puzzle, such as the Save to Files filename field. 38 @MainActor static func setCaptureSuspended(_ suspended: Bool) { 39 isCaptureSuspended = suspended 40 NotificationCenter.default.post( 41 name: suspended ? suspendCaptureNotification : resumeCaptureNotification, 42 object: nil 43 ) 44 } 45 46 final class KeyCaptureView: UIView { 47 var onPress: ((HardwareKeyboardEvent) -> Bool)? 48 private var isActive = true 49 private var isSuspended = false 50 51 override init(frame: CGRect) { 52 super.init(frame: frame) 53 isSuspended = HardwareKeyboardInputView.isCaptureSuspended 54 observeCaptureNotifications() 55 } 56 57 required init?(coder: NSCoder) { 58 super.init(coder: coder) 59 isSuspended = HardwareKeyboardInputView.isCaptureSuspended 60 observeCaptureNotifications() 61 } 62 63 deinit { 64 NotificationCenter.default.removeObserver(self) 65 } 66 67 override var canBecomeFirstResponder: Bool { true } 68 69 override var keyCommands: [UIKeyCommand]? { 70 let letters = "abcdefghijklmnopqrstuvwxyz".map { 71 UIKeyCommand( 72 input: String($0), 73 modifierFlags: [], 74 action: #selector(handleKeyCommand(_:)) 75 ) 76 } 77 78 let digits = "0123456789".map { 79 UIKeyCommand( 80 input: String($0), 81 modifierFlags: [], 82 action: #selector(handleKeyCommand(_:)) 83 ) 84 } 85 86 // Undo/redo (⌘Z, ⇧⌘Z) are vended by the app menu (PuzzleCommands) so 87 // they show in the hold-⌘ shortcut overlay; leaving them out here 88 // lets those presses bubble up to that menu command. 89 return letters + digits + [ 90 UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))), 91 UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))), 92 UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: .command, action: #selector(handleKeyCommand(_:))), 93 UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: .command, action: #selector(handleKeyCommand(_:))), 94 UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))), 95 UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))), 96 UIKeyCommand(input: "\u{8}", modifierFlags: [], action: #selector(handleKeyCommand(_:))), 97 UIKeyCommand(input: "\u{7F}", modifierFlags: [], action: #selector(handleKeyCommand(_:))), 98 UIKeyCommand(input: "\t", modifierFlags: [], action: #selector(handleKeyCommand(_:))), 99 UIKeyCommand(input: "\t", modifierFlags: .shift, action: #selector(handleKeyCommand(_:))), 100 UIKeyCommand(input: " ", modifierFlags: [], action: #selector(handleKeyCommand(_:))), 101 UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(handleKeyCommand(_:))), 102 UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(handleKeyCommand(_:))) 103 ] 104 } 105 106 override func didMoveToWindow() { 107 super.didMoveToWindow() 108 ensureFirstResponder() 109 } 110 111 func setActive(_ active: Bool) { 112 isActive = active 113 if active { 114 ensureFirstResponder() 115 } else if isFirstResponder { 116 resignFirstResponder() 117 } 118 } 119 120 func ensureFirstResponder() { 121 guard isActive, !isSuspended, window != nil, !isFirstResponder else { return } 122 Task { @MainActor in 123 guard self.isActive, 124 !self.isSuspended, 125 self.window != nil, 126 !self.isFirstResponder else { return } 127 self.becomeFirstResponder() 128 } 129 } 130 131 private func observeCaptureNotifications() { 132 NotificationCenter.default.addObserver( 133 self, 134 selector: #selector(suspendCapture), 135 name: HardwareKeyboardInputView.suspendCaptureNotification, 136 object: nil 137 ) 138 NotificationCenter.default.addObserver( 139 self, 140 selector: #selector(resumeCapture), 141 name: HardwareKeyboardInputView.resumeCaptureNotification, 142 object: nil 143 ) 144 } 145 146 @objc private func suspendCapture() { 147 isSuspended = true 148 if isFirstResponder { 149 resignFirstResponder() 150 } 151 } 152 153 @objc private func resumeCapture() { 154 isSuspended = false 155 ensureFirstResponder() 156 } 157 158 override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) { 159 var unhandled: [UIPress] = [] 160 161 for press in presses { 162 guard let key = press.key else { 163 unhandled.append(press) 164 continue 165 } 166 167 let event = HardwareKeyboardEvent( 168 keyCode: key.keyCode, 169 charactersIgnoringModifiers: key.charactersIgnoringModifiers, 170 modifierFlags: key.modifierFlags 171 ) 172 173 if onPress?(event) != true { 174 unhandled.append(press) 175 } 176 } 177 178 if !unhandled.isEmpty { 179 super.pressesBegan(Set(unhandled), with: event) 180 } 181 } 182 183 @objc private func handleKeyCommand(_ command: UIKeyCommand) { 184 guard let event = HardwareKeyboardEvent( 185 characters: command.input ?? "", 186 modifierFlags: command.modifierFlags 187 ) else { return } 188 _ = onPress?(event) 189 } 190 } 191 } 192 193 private extension HardwareKeyboardEvent { 194 init?(characters: String, modifierFlags: UIKeyModifierFlags) { 195 let keyCode: UIKeyboardHIDUsage 196 197 switch characters { 198 case UIKeyCommand.inputLeftArrow: 199 keyCode = .keyboardLeftArrow 200 case UIKeyCommand.inputRightArrow: 201 keyCode = .keyboardRightArrow 202 case UIKeyCommand.inputUpArrow: 203 keyCode = .keyboardUpArrow 204 case UIKeyCommand.inputDownArrow: 205 keyCode = .keyboardDownArrow 206 case UIKeyCommand.inputEscape: 207 keyCode = .keyboardEscape 208 case "\u{8}": 209 keyCode = .keyboardDeleteOrBackspace 210 case "\u{7F}": 211 keyCode = .keyboardDeleteForward 212 case "\t": 213 keyCode = .keyboardTab 214 case " ": 215 keyCode = .keyboardSpacebar 216 case "\r": 217 keyCode = .keyboardReturnOrEnter 218 case "a"..."z", "A"..."Z": 219 guard let scalar = characters.uppercased().unicodeScalars.first, 220 let code = UIKeyboardHIDUsage(rawValue: Int(scalar.value - 65) + UIKeyboardHIDUsage.keyboardA.rawValue) else { 221 return nil 222 } 223 keyCode = code 224 case "1"..."9": 225 // HID usages run keyboard1…keyboard9 contiguously (keyboard0 sits 226 // after, handled separately below). 227 guard let digit = characters.first?.wholeNumberValue, 228 let code = UIKeyboardHIDUsage(rawValue: UIKeyboardHIDUsage.keyboard1.rawValue + (digit - 1)) else { 229 return nil 230 } 231 keyCode = code 232 case "0": 233 keyCode = .keyboard0 234 default: 235 return nil 236 } 237 238 self.init( 239 keyCode: keyCode, 240 charactersIgnoringModifiers: characters, 241 modifierFlags: modifierFlags 242 ) 243 } 244 }