crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

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 }