Skip to content

Commit 8d5bf6b

Browse files
authored
[Woo POS][Barcodes] i2 Use GameController framework for detecting scanner input (#15877)
2 parents b0430e4 + 99f3fb6 commit 8d5bf6b

File tree

7 files changed

+968
-693
lines changed

7 files changed

+968
-693
lines changed

WooCommerce/Classes/POS/Presentation/Barcode Scanning/BarcodeScannerContainer.swift

Lines changed: 11 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -39,79 +39,35 @@ struct BarcodeScannerContainerRepresentable: UIViewControllerRepresentable {
3939
let configuration: HIDBarcodeParserConfiguration
4040
let onScan: (Result<String, Error>) -> Void
4141

42-
func makeUIViewController(context: Context) -> BarcodeScannerHostingController {
43-
let controller = BarcodeScannerHostingController(
42+
func makeUIViewController(context: Context) -> UIViewController {
43+
return GameControllerBarcodeScannerHostingController(
4444
configuration: configuration,
4545
onScan: onScan
4646
)
47-
return controller
4847
}
4948

50-
func updateUIViewController(_ uiViewController: BarcodeScannerHostingController, context: Context) {}
49+
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
5150
}
5251

53-
/// A UIHostingController that handles keyboard input events for barcode scanning.
54-
/// This controller captures keyboard input and interprets it as barcode data when a terminating
55-
/// character is detected.
56-
class BarcodeScannerHostingController: UIHostingController<EmptyView> {
57-
private let configuration: HIDBarcodeParserConfiguration
58-
private let scanner: HIDBarcodeParser
52+
/// A UIHostingController that handles GameController keyboard input events for barcode scanning.
53+
/// This controller uses GameController framework exclusively for language-independent barcode scanning.
54+
final class GameControllerBarcodeScannerHostingController: UIHostingController<EmptyView> {
55+
private var gameControllerBarcodeObserver: GameControllerBarcodeObserver?
5956

6057
init(
6158
configuration: HIDBarcodeParserConfiguration,
6259
onScan: @escaping (Result<String, Error>) -> Void
6360
) {
64-
self.configuration = configuration
65-
self.scanner = HIDBarcodeParser(configuration: configuration,
66-
onScan: onScan)
6761
super.init(rootView: EmptyView())
62+
63+
gameControllerBarcodeObserver = GameControllerBarcodeObserver(configuration: configuration, onScan: onScan)
6864
}
6965

7066
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
7167
fatalError("init(coder:) has not been implemented")
7268
}
7369

74-
override var canBecomeFirstResponder: Bool { true }
75-
76-
override func viewDidAppear(_ animated: Bool) {
77-
super.viewDidAppear(animated)
78-
becomeFirstResponder()
79-
}
80-
81-
override func viewDidDisappear(_ animated: Bool) {
82-
super.viewDidDisappear(animated)
83-
resignFirstResponder()
84-
}
85-
86-
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
87-
/// We don't call super here because it helps prevent the system from hiding the software keyboard when
88-
/// a textfield is next used.
89-
}
90-
91-
/// Handles the end of keyboard press events, interpreting them as barcode input.
92-
/// When a terminating character is detected, the accumulated buffer is treated as a complete
93-
/// barcode and passed to the onScan callback.
94-
/// We don't call `super` here because we don't other responder chain items to handle our barcode as well,
95-
/// as this could cause unexpected behavior.
96-
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
97-
/// While a scanner should just "press" each key once, in theory it's possible for presses to be cancelled
98-
/// or change between the `began` call and the `ended` call.
99-
/// It's better practice for barcode scanning to only consider the presses when they end.
100-
for press in presses {
101-
guard let key = press.key else { continue }
102-
scanner.processKeyPress(key)
103-
}
104-
}
105-
106-
override func pressesChanged(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
107-
super.pressesChanged(presses, with: event)
108-
}
109-
110-
/// `pressesCancelled` is rarely called, but Apple's documentation suggests it's possible and that crashes may occur if it's not handled.
111-
/// It makes sense to clear the buffer when this happens.
112-
/// We call super in case other presses are handled elsewhere in the responder chain.
113-
override func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
114-
scanner.cancel()
115-
super.pressesCancelled(presses, with: event)
70+
deinit {
71+
gameControllerBarcodeObserver = nil
11672
}
11773
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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

Comments
 (0)