June 26, 2021

Handheld barcode scanners with Turbo-iOS 📦

Some of the coolest things to build are apps with physical hardware. In the past I've used Turbo-iOS (and it's predecessor Turbolinks-iOS) to build a full-fledged POS system for Spina Shop. More recently I've created a specialised app for picking orders in our warehouse.
Spina Warehouse

Employees can create an order pick list containing multiple orders. The app then figures out the most efficient way to pick those orders. In order for the app to recognise products you need a form of identification. Luckily, (almost) all products have a barcode (EAN).
Barcode scanning

We decided to use handheld barcode scanners because of their reliability and ease of use in a warehouse environment. When connected to an iOS device, they are recognised as a keyboard. After scanning a barcode, the scanner enters all characters at once, followed by the return key. You could "catch" this inside a textfield and use that as your input. This works, but this only works when your cursor is focused on a textfield.

It's better to recreate this functionality in Swift. By using keyCommands we can recognise a barcode scanner's input anywhere in our app. Regardless of focus.

Step 1. Create a VisitableViewController

In order to add functionality to our visitable view, we need to have a VisitableViewController. In my app I want to scan barcodes when I'm in a modal, so I'm going to create a ModalController like this:
import UIKit
import Turbo

class ModalController: Turbo.VisitableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Close", style: .plain, target: self, action: #selector(dismissModal))
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }
    
    @objc func dismissModal() {
        dismiss(animated: true)
    }

}

Step 2. Become the first responder

In order to receive keyboard inputs, your view must become the so-called first responder.
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    becomeFirstResponder()
}

override var canBecomeFirstResponder: Bool {
    return true
}

Step 3. Recognise character inputs

iOS handles character inputs a bit differently than you might expect. Uppercase and lowercase characters are recognised by literally adding the shift-key as a modifier. That's why we have two separate methods to catch both lowercase and uppercase characters.
override var keyCommands: [UIKeyCommand]? {
    var commands:[UIKeyCommand] = []
    let characters = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]

    for character in characters {
        let txt = character as String
        if Int(txt) == nil {
            commands.append(UIKeyCommand(input: txt, modifierFlags: [UIKeyModifierFlags.shift], action: #selector(uppercasedKeyPressed(_:))))
            commands.append(UIKeyCommand(input: txt, modifierFlags: [], action: #selector(lowercasedKeyPressed(_:))))
        } else {
            commands.append(UIKeyCommand(input: txt, modifierFlags: [], action: #selector(uppercasedKeyPressed(_:))))
        }
    }
    commands.append(UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(returnPressed)))
    return commands
}

@objc func lowercasedKeyPressed(_ input: UIKeyCommand) {
    // Input lowercase key
}

@objc func uppercasedKeyPressed(_ input: UIKeyCommand) {
    // Input uppercase key
}

@objc func returnPressed() {
    // Pressed enter
}

Step 4. Store barcode

Add an array to your class so you can store the barcode characters.
class ModalController: VisitableViewController {
    var barcode = Array<String>()

    // ...

    @objc func lowercasedKeyPressed(_ input: UIKeyCommand) {
        self.barcode.append(input.input!.lowercased())
    }

    @objc func uppercasedKeyPressed(_ input: UIKeyCommand) {
        self.barcode.append(input.input!)
    }
}

Step 5. Submit barcode

After pressing the return key, I want to pass my barcode to my Rails view so I can use it in my server code.
func submitBarcode(_ barcode: String) {
    visitableView.webView?.evaluateJavaScript("document.getElementById('scanner').scanner.scan('\(barcode)')", completionHandler: nil)
}

@objc func returnPressed() {
    // Submit barcode via JavaScript
    submitBarcode(barcode.joined())
    
    // Reset barcode after return
    self.barcode = []
}

Step 6. Use a Stimulus controller to receive the scan input

In my Rails webview I've used a Stimulus controller to receive the input of the barcode scanner and do something with it. Here's a simplified version:
<div id="scanner" data-controller="scanner"></div>

import { Controller } from "stimulus"

export default class extends Controller {
  connect() {
    this.element[this.identifier] = this
  }
  
  scan(code) {
    console.log(`My barcode: ${code}!`)
  }
}

Bonus: add sound!

There's a neat little Swift library called SwiftySound that makes it dead simple to add sounds to your native code. You can use it to create a helpful sound when scanning products.
import SwiftySound

class ModalController: VisitableViewController {
  
    // ...
  
    func submitBarcode(_ barcode: String) {
        Sound.play(file: "scanSound.mp3")
        visitableView.webView?.evaluateJavaScript("document.getElementById('scanner').scanner.scan('\(barcode)')", completionHandler: nil)
    }

}