crossmate

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

HardwareKeyboardInputView.swift (5886B)


      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 
     13     func makeUIView(context: Context) -> KeyCaptureView {
     14         let view = KeyCaptureView()
     15         view.onPress = onPress
     16         return view
     17     }
     18 
     19     func updateUIView(_ uiView: KeyCaptureView, context: Context) {
     20         uiView.onPress = onPress
     21         uiView.ensureFirstResponder()
     22     }
     23 
     24     final class KeyCaptureView: UIView {
     25         var onPress: ((HardwareKeyboardEvent) -> Bool)?
     26 
     27         override var canBecomeFirstResponder: Bool { true }
     28 
     29         override var keyCommands: [UIKeyCommand]? {
     30             let letters = "abcdefghijklmnopqrstuvwxyz".map {
     31                 UIKeyCommand(
     32                     input: String($0),
     33                     modifierFlags: [],
     34                     action: #selector(handleKeyCommand(_:))
     35                 )
     36             }
     37 
     38             let undo = UIKeyCommand(input: "z", modifierFlags: .command, action: #selector(handleKeyCommand(_:)))
     39             undo.discoverabilityTitle = "Undo Move"
     40             let redo = UIKeyCommand(input: "z", modifierFlags: [.command, .shift], action: #selector(handleKeyCommand(_:)))
     41             redo.discoverabilityTitle = "Redo Move"
     42 
     43             return letters + [
     44                 undo,
     45                 redo,
     46                 UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     47                 UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     48                 UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: .command, action: #selector(handleKeyCommand(_:))),
     49                 UIKeyCommand(input: UIKeyCommand.inputRightArrow, modifierFlags: .command, action: #selector(handleKeyCommand(_:))),
     50                 UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     51                 UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     52                 UIKeyCommand(input: "\u{8}", modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     53                 UIKeyCommand(input: "\u{7F}", modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     54                 UIKeyCommand(input: "\t", modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     55                 UIKeyCommand(input: "\t", modifierFlags: .shift, action: #selector(handleKeyCommand(_:))),
     56                 UIKeyCommand(input: " ", modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     57                 UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(handleKeyCommand(_:))),
     58                 UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(handleKeyCommand(_:)))
     59             ]
     60         }
     61 
     62         override func didMoveToWindow() {
     63             super.didMoveToWindow()
     64             ensureFirstResponder()
     65         }
     66 
     67         func ensureFirstResponder() {
     68             guard window != nil, !isFirstResponder else { return }
     69             Task { @MainActor in
     70                 self.becomeFirstResponder()
     71             }
     72         }
     73 
     74         override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
     75             var unhandled: [UIPress] = []
     76 
     77             for press in presses {
     78                 guard let key = press.key else {
     79                     unhandled.append(press)
     80                     continue
     81                 }
     82 
     83                 let event = HardwareKeyboardEvent(
     84                     keyCode: key.keyCode,
     85                     charactersIgnoringModifiers: key.charactersIgnoringModifiers,
     86                     modifierFlags: key.modifierFlags
     87                 )
     88 
     89                 if onPress?(event) != true {
     90                     unhandled.append(press)
     91                 }
     92             }
     93 
     94             if !unhandled.isEmpty {
     95                 super.pressesBegan(Set(unhandled), with: event)
     96             }
     97         }
     98 
     99         @objc private func handleKeyCommand(_ command: UIKeyCommand) {
    100             guard let event = HardwareKeyboardEvent(
    101                 characters: command.input ?? "",
    102                 modifierFlags: command.modifierFlags
    103             ) else { return }
    104             _ = onPress?(event)
    105         }
    106     }
    107 }
    108 
    109 private extension HardwareKeyboardEvent {
    110     init?(characters: String, modifierFlags: UIKeyModifierFlags) {
    111         let keyCode: UIKeyboardHIDUsage
    112 
    113         switch characters {
    114         case UIKeyCommand.inputLeftArrow:
    115             keyCode = .keyboardLeftArrow
    116         case UIKeyCommand.inputRightArrow:
    117             keyCode = .keyboardRightArrow
    118         case UIKeyCommand.inputUpArrow:
    119             keyCode = .keyboardUpArrow
    120         case UIKeyCommand.inputDownArrow:
    121             keyCode = .keyboardDownArrow
    122         case UIKeyCommand.inputEscape:
    123             keyCode = .keyboardEscape
    124         case "\u{8}":
    125             keyCode = .keyboardDeleteOrBackspace
    126         case "\u{7F}":
    127             keyCode = .keyboardDeleteForward
    128         case "\t":
    129             keyCode = .keyboardTab
    130         case " ":
    131             keyCode = .keyboardSpacebar
    132         case "\r":
    133             keyCode = .keyboardReturnOrEnter
    134         case "a"..."z", "A"..."Z":
    135             guard let scalar = characters.uppercased().unicodeScalars.first,
    136                   let code = UIKeyboardHIDUsage(rawValue: Int(scalar.value - 65) + UIKeyboardHIDUsage.keyboardA.rawValue) else {
    137                 return nil
    138             }
    139             keyCode = code
    140         default:
    141             return nil
    142         }
    143 
    144         self.init(
    145             keyCode: keyCode,
    146             charactersIgnoringModifiers: characters,
    147             modifierFlags: modifierFlags
    148         )
    149     }
    150 }