|
| 1 | +import Foundation |
| 2 | +import GameController |
| 3 | + |
| 4 | +/// An observer that uses the `GameController` framework to monitor for connected barcode scanners/keyboards |
| 5 | +/// and parse their input into barcode strings. |
| 6 | +/// |
| 7 | +/// This class handles the low-level details of observing keyboard connections, processing raw `GCKeyCode` inputs, |
| 8 | +/// and using the `GameControllerBarcodeParser` to produce a final barcode. |
| 9 | +/// |
| 10 | +final class GameControllerBarcodeObserver { |
| 11 | + /// A closure that is called when a barcode scan is completed. |
| 12 | + /// The result will be a `success` with the barcode string or a `failure` with an error. |
| 13 | + let onScan: (Result<String, Error>) -> Void |
| 14 | + |
| 15 | + /// Track the coalesced keyboard and its parser |
| 16 | + /// According to Apple's documentation, all connected keyboards are coalesced into one keyboard object |
| 17 | + /// (GCKeyboard.coalesced), so notification about connection/disconnection will only be delivered once |
| 18 | + /// until the last keyboard disconnects. |
| 19 | + private var coalescedKeyboard: GCKeyboard? |
| 20 | + private var barcodeParser: GameControllerBarcodeParser? |
| 21 | + private let configuration: HIDBarcodeParserConfiguration |
| 22 | + |
| 23 | + /// Tracks current shift state to be applied to the next character key |
| 24 | + private var isShiftPressed: Bool = false |
| 25 | + |
| 26 | + /// Initializes a new barcode scanner observer. |
| 27 | + /// - Parameters: |
| 28 | + /// - configuration: The configuration to use for the barcode parser. Defaults to the standard configuration. |
| 29 | + /// - onScan: The closure to be called when a scan is completed. |
| 30 | + init(configuration: HIDBarcodeParserConfiguration = .default, onScan: @escaping (Result<String, Error>) -> Void) { |
| 31 | + self.onScan = onScan |
| 32 | + self.configuration = configuration |
| 33 | + addObservers() |
| 34 | + setupCoalescedKeyboard() |
| 35 | + } |
| 36 | + |
| 37 | + deinit { |
| 38 | + removeObservers() |
| 39 | + cleanupKeyboard() |
| 40 | + } |
| 41 | + |
| 42 | + /// Starts observing for keyboard connection and disconnection events. |
| 43 | + private func addObservers() { |
| 44 | + NotificationCenter.default.addObserver( |
| 45 | + self, |
| 46 | + selector: #selector(handleKeyboardDidConnect), |
| 47 | + name: .GCKeyboardDidConnect, |
| 48 | + object: nil |
| 49 | + ) |
| 50 | + NotificationCenter.default.addObserver( |
| 51 | + self, |
| 52 | + selector: #selector(handleKeyboardDidDisconnect), |
| 53 | + name: .GCKeyboardDidDisconnect, |
| 54 | + object: nil |
| 55 | + ) |
| 56 | + } |
| 57 | + |
| 58 | + /// Stops observing for keyboard events. |
| 59 | + private func removeObservers() { |
| 60 | + NotificationCenter.default.removeObserver(self, name: .GCKeyboardDidConnect, object: nil) |
| 61 | + NotificationCenter.default.removeObserver(self, name: .GCKeyboardDidDisconnect, object: nil) |
| 62 | + } |
| 63 | + |
| 64 | + /// Sets up the coalesced keyboard if one is available at initialization. |
| 65 | + private func setupCoalescedKeyboard() { |
| 66 | + if let keyboard = GCKeyboard.coalesced { |
| 67 | + setupKeyboard(keyboard) |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + /// Handles the connection of a keyboard (coalesced). |
| 72 | + @objc private func handleKeyboardDidConnect(_ notification: Notification) { |
| 73 | + guard let keyboard = notification.object as? GCKeyboard else { |
| 74 | + return |
| 75 | + } |
| 76 | + setupKeyboard(keyboard) |
| 77 | + } |
| 78 | + |
| 79 | + /// Handles the disconnection of the keyboard (coalesced). |
| 80 | + @objc private func handleKeyboardDidDisconnect(_ notification: Notification) { |
| 81 | + cleanupKeyboard() |
| 82 | + } |
| 83 | + |
| 84 | + /// Sets up the coalesced keyboard to handle key press events. |
| 85 | + /// - Parameter keyboard: The coalesced `GCKeyboard` to set up. |
| 86 | + private func setupKeyboard(_ keyboard: GCKeyboard) { |
| 87 | + // Clean up any existing setup first |
| 88 | + cleanupKeyboard() |
| 89 | + |
| 90 | + coalescedKeyboard = keyboard |
| 91 | + barcodeParser = GameControllerBarcodeParser(configuration: configuration, onScan: onScan) |
| 92 | + |
| 93 | + keyboard.keyboardInput?.keyChangedHandler = { [weak self] _, _, keyCode, pressed in |
| 94 | + guard let self = self else { return } |
| 95 | + |
| 96 | + if keyCode == .leftShift || keyCode == .rightShift { |
| 97 | + self.isShiftPressed = pressed |
| 98 | + return |
| 99 | + } |
| 100 | + |
| 101 | + guard pressed else { return } |
| 102 | + |
| 103 | + self.barcodeParser?.processKeyPress(keyCode, isShiftPressed: isShiftPressed) |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + /// Cleans up the coalesced keyboard and its parser. |
| 108 | + private func cleanupKeyboard() { |
| 109 | + coalescedKeyboard?.keyboardInput?.keyChangedHandler = nil |
| 110 | + barcodeParser?.cancel() |
| 111 | + coalescedKeyboard = nil |
| 112 | + barcodeParser = nil |
| 113 | + isShiftPressed = false |
| 114 | + } |
| 115 | +} |
0 commit comments