iOSアプリにバーコードスキャナーを接続したら日本語がしんどかった件

ZAICOでは、Android・iOS・Rubyエンジニアを絶賛募集中です! 詳しくは、採用ページをご覧ください。

好きな場所で働こう

こんにちは。ZAICO開発チームの新人兼ボケ担当いくえもんです。

タイトルから異世界の風を感じた方、すみません。ほんのり香る程度で本ブログと異世界は全く関係ありません!イメージを壊されたくない方はここで現実世界にお帰りください m(_ _)m

ZAICOに入社して初めてバーコードスキャナーなるものを手に取って使ってみたら色々なトラップに引っかかったので、今後スキャナーを使うかもしれない迷える子スライムちゃん達の道標になればとブログにしてみました。

魔王さまからのご要望

今回やりたかったことはこんな感じです。

  • iPhoneとバーコードスキャナーをBluetooth接続してQRコードを読み取りたい(あの大量の魔物達はQRコードで管理されていたんですね!ZAICOアプリもお使いでしょうか??)
  • 大文字と小文字を区別したい(そうですよね。スライムとキングスライムの見分けがつかないと困りますもんね!)
  • 日本語キーボードを使いたい(魔王さまは日本文化に造詣の深い方なのですね!流暢な日本語からそうじゃないかと思っていました!!)
  • スキャナー利用中はソフトウェアキーボードなんか見たくない(背景に推しスライムのブロマイド画像を設定されているんですね。少しでも長くお気に入りのスライムを見ていたいですもんね!)

序章

おふざけはこの辺にして早速本題に入りましょう。

そもそもバーコードスキャナーとiPhoneをBluetooth接続するとバーコードスキャナーはどういう扱いになるのでしょうか?答えは、外部入力装置です。つまり、外付けキーボードと同じ扱いになります。このため、バーコードスキャナーからの入力はUITextFieldなどを使って簡単に受け取ることができます。魔王さまが色々と妥協してくださればこれだけでOKなのですが、なかなかそういう訳にはいきません。なので、ここからスライムがご要望を叶えるために旅した冒険の内容をお伝えします。

長々読んでいられないというせっかちな子スライムちゃんは最後の段落までルーラしちゃってくださいね(既にドラ◯エ化)。

01話 まずはスキャナーを入手しよう

何はともあれ武器がないと冒険には出られません。例えそれが木の棒と鍋の蓋であったとしても!というわけで、早速スキャナーを注文していただきました。検証に使ったスキャナーはこちらです:Tera 1D 2D QRバーコードスキャナー 表示ディスプレイ搭載

バーコードスキャナーとiPhoneの接続方法は弊社ヘルプページ外付けバーコードリーダーを使う(iPhone版)をご参照ください。

ただし、接続が終わっても安心はできません。第一のトラップはここにありました。それは、スキャナーを接続した途端にソフトウェアキーボードが全く表示されなくなる問題です。スキャナー利用中に表示されないのはいいのですが、文字入力したいときにも表示されない問題が発生することがあります。そんな時はスキャナーのトリガーを2回、3回、4回と引いてみてください。スキャナーの機種によってソフトウェアキーボードを出す方法は異なりますが、検証に使ったスキャナーは2回トリガーを素早く引くとキーボードを表示することができました。

02話 UIKeyCommandを使ってみる

Googleで外付けバーコードリーダーの扱いについて検索するとUIKeyCommandを利用する方法がヒットすることがよくあります。弊社でも当初はこちらの方法を使って実装をしていました。下のような方法です。

class ViewController: UIViewController {
    // 読み取った文字を保存する配列
    private var codes: [String] = []

    // ViewControllerで入力を受け取れるようにする
    override var canBecomeFirstResponder: Bool {
        return true
    }

    // UIKeyCommandをオーバーライドし、文字を読み取るたびに指定の処理を実行する
    override var keyCommands: [UIKeyCommand]? {
        return self._keyCommands
    }
    
    private var _keyCommands: [UIKeyCommand] = {
        // スキャナーで受け取る文字配列を作成する。
        let characters = CharacterSet(charactersIn: UnicodeScalar(0x20)...UnicodeScalar(0x7f-0x20)) // ASCII文字の記号、半角数字、大文字
            .union(CharacterSet.newlines) // 改行文字
        
        let chars = (0x00...0x7f-0x20)
            .map { UnicodeScalar($0) }
            .filter { characters.contains($0) }
        
        // これで1文字入力されるたびにhandleKeyCommandが呼び出される
        return chars.map {
            UIKeyCommand(input: String($0), modifierFlags: [], action: #selector(handleKeyCommand))
        }
    }()
    
    @objc func handleKeyCommand(sender: UIKeyCommand) {
        if sender.input?.rangeOfCharacter(from: CharacterSet.newlines)?.isEmpty == false {
            // 改行文字を発見。コードを全て受け取ったとみなす。
            debugPrint(codes.joined())
            codes = []
        } else {
            codes.append(sender.input ?? "")
        }
    }
}

さて、これは何がいいんでしょうか?

UIKeyCommandはこのように定義されています。

An object that specifies a key press perform on a hardware keyboard and the resulting action.

つまり、ハードウェアキーボードのキーが押されたイベント(スキャナーの読み取り)と対応するアクションを定義することができます。これにより、ユーザーがわざわざ入力フィールドなどをタップしなくても、また、ソフトウェアキーボードを表示しなくてもバーコードを読み取ることができます!

では、何が問題なんでしょうか?上で記載したように、これはキーボードのキーが押されたイベントを受け取ります。キーボード上ではsもSも同じキーにマッピングされていますよね。つまり、sとS、スライムとキングスライムを見分けることができないんです!!がーーん。。。

ちなみに、この方法だとsとSのどちらを受け取るんでしょうか?公式ドキュメントに記載はありませんが、先にkey command objectとして作成された方を優先するようです。つまり、コードの25・26行目で先に小文字のオブジェクトを作成したら小文字が、先に大文字のオブジェクトを作成したら大文字が検知されます。

03話 UITextFieldを使ってみる

スライムとキングスライムを見分けられないのは大問題なので、次の村で情報収集をしましょう。

そもそもスキャナーは外部入力装置なので、サイズ0のUITextFieldを使って入力を受け取ってみたらどうでしょうか?UITextFieldはもちろん大文字小文字の区別ができるのでいけそうな気がします!

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        let hiddenTextView = UITextField(frame: .zero)  // ユーザーには見えないUITextFieldを作成
        hiddenTextView.autocapitalizationType = .none   // 1文字目が大文字に変換されるのを防ぐ
        self.view.addSubview(hiddenTextView)
        hiddenTextView.addTarget(self, action: #selector(textDidEndEditing(_:)), for: .editingDidEndOnExit) // 改行文字があったらtextDidEndEditingメソッドを呼ぶ
        hiddenTextView.becomeFirstResponder()   // フォーカスしてスキャナーの入力を読み取る
    }

    @objc func textDidEndEditing(_ textField: UITextField) {
        debugPrint(textField.text ?? "nil")
    }
}

うーーーん。思ったほど上手くいきません。

まず、8行目でUITextFieldをFirstResponderにした時点でソフトウェアキーボードが出てきてしまいます。また、7行目で.editingDidEndOnExitを検知するとhiddenTextViewからフォーカスが外れてしまい、連続読み込みができません。

魔王さまがお怒りのようなので次の村で情報収集しましょう!

04話 UIKeyInputを使ってみる

Appleの公式ドキュメントによると、UIKeyInputは次のように定義されています。

A set of methods a responder uses to implement simple text entry.

つまり、テキスト入力に対する処理を定義することができます。特にinsertText関数は表示するテキストに文字を追加するための関数なので、これを使ってスキャナーの入力を処理してみましょう。

class ViewController: UIViewController {
    // 読み取った文字を保存する配列
    fileprivate var codes: [String] = []
}

extension ViewController: UIKeyInput {
    func deleteBackward() {
    }
    
    public var hasText: Bool {
        false
    }
    
    // 1文字入力されるたびに呼び出される
    public func insertText(_ text: String) {
        if (text.rangeOfCharacter(from: CharacterSet.newlines) != .none) {
            // 改行文字が見つかったので、コードを全て読み取ったとみなす
            debugPrint(codes.joined())
            codes = []
        } else {
            codes.append(text)
        }
    }
    
    // ViewController全体にフォーカスを当ててスキャナーからの読み込みを可能にする
    open override var canBecomeFirstResponder: Bool { true }
    
    // InputViewにUIViewを設定すると、FirstResponderになっても(フォーカスが当たっても)キーボードを非表示にすることができる
    open override var inputView: UIView? {
        return UIView()
    }
}

なんだかいい感じですね!大文字小文字も区別できるし、キーボードも表示されません。もちろんQRコードも読み取れています!

でもここに最後のトラップが待っていました。なんと、スキャナーを使う前に日本語キーボードを開くと改行文字以外のスキャナーからの入力を受け取れなくなってしまうのです!!

日本語キーボード以外は試していないのでわかりませんが、2バイト文字を入力するキーボードでは同じ問題が発生すると予想されます。英数字以外の特殊な文字入力があるキーボードでも何かしら問題が起きるのかもしれません。そして、世界の大半の人たちは日本語キーボードを使わないので、この問題への対処法をGoogle先生にお尋ねしても全く見つかりませんでした orz… 中国語とかで検索したら解答が見つかったかもしれませんが。。。

公式ドキュメントに記載はないですが、UIKeyInputの処理はデフォルトのキーボードに依存するようです。デフォルトのキーボードに読み取り対象となる半角英数がない場合、全ての文字が無視されてしまいます!!!ちなみに、このデフォルトキーボードとはユーザーが最後に自主的に開いたキーボードです。なので、画面を開いた瞬間に一瞬プログラムで英語キーボードを開いて閉じるなどの姑息な手段は通用しません(←すでに実験済み 涙)。

魔王様の愛を感じますね。それでは、最後の村を出発して魔王城へ向かいましょう。

最終話 頼みの綱はUITextInputTraits

04話のUIKeyInputの動作があまりにも良かったので未練タラタラでストーカーと化したスライムちゃんはUIKeyInputを使ってキーボードを無理やり英語に固定する方法を模索しました。でも安心してください。監禁とかはしてません!

色々方法を探しながらたどり着いたUIResponder酒場で出会ったのがUITextInputTraitsちゃんでした。Appleの公式ドキュメントには

A set of methods that defines features for keyboard input to a text object.

と紹介されています。ほうほう、キーボード入力を定義できるとな。では早速やってみよう!というわけで、下のようにカスタムクラスを定義しました。

protocol ScannerCustomInputViewDelegate: class {
    func handleInsertedText(_ text: String, inputView: ScannerCustomInputView)
}

class ScannerCustomInputView: UIView {
    weak var delegate: ScannerCustomInputViewDelegate?
    
    let keyboardHinderView = UIView()
}

// ここは04話とほぼ同じ
extension ScannerCustomInputView: UIKeyInput {
    
    // First Responderになることで読み取りモード(キーボード入力ON)にする
    override var canBecomeFirstResponder: Bool { return true }
    
    var hasText: Bool { false }

    // 1文字入力されるたびにhandleInsertedTextを呼び出す
    func insertText(_ text: String) {
        delegate?.handleInsertedText(text, inputView: self)
    }
    
    func deleteBackward() {}
    
    // inputViewをUIViewにすることによりキーボードを非表示とする
    open override var inputView: UIView? { return keyboardHinderView }
}

// ここでキーボードの動作を定義
extension ScannerCustomInputView: UITextInputTraits {
    
    var keyboardType: UIKeyboardType {
        get { return .asciiCapable } // 英語キーボードを指定
        set { }
    }
    
    // キーボードを英語に限定する
    override var textInputMode: UITextInputMode? {
        let locale = Locale(identifier: "en_US")
        
        return UITextInputMode.activeInputModes.first(where: { $0.primaryLanguage?.contains(locale.languageCode ?? "en" ) ?? false }) ?? super.textInputMode
    }
}

あとはこのカスタムクラスをViewControllerのViewに追加するだけですね。簡単にViewController側もご紹介しておきます。

class ViewController: UIViewController {
    // 読み取った文字を保存する配列
    fileprivate var codes: [String] = []
    
    override func viewDidLoad() {
        let inputView = ScannerCustomInputView()    // バーコードリーダーからの入力を受け取るView。これはユーザーには見えない。
        inputView.delegate = self
        inputView.becomeFirstResponder()    // このViewでスキャナからのインプットを受け取る
        self.view.addSubview(inputView)
    }
}

extension ViewController: ScannerCustomInputViewDelegate {
    // バーコードリーダーの入力を1文字ずつ処理する
    func handleInsertedText(_ text: String, inputView: ScannerCustomInputView) {
        // 改行文字を見つけるとバーコードを全て読み込んだと見做す
        if (text.rangeOfCharacter(from: CharacterSet.newlines) != .none) {
            if codes.count == 0 {
                // アプリ起動後一度もキーボードを開いたり入力を受け付けていないと、textInputModeが正しく評価されないことがある
                // このためキーボードが日本語に設定されたままとなり、改行文字だけが読み込まれる
                // この場合、InputViewを再読み込みすることでtextInputModeを再評価する
                inputView.reloadInputViews()
                return
            }
            debugPrint(codes.joined())
            codes = []
        } else {
            codes.append(text)
        }
    }
}

以上、スライムちゃんの冒険はいかがでしたか?これからバーコードスキャナーをお使いの方の参考になれば幸いです!

最後までご精読いただきありがとうございました m(_ _)m

ZAICOでは、新しいテクノロジーの力でモノの状態・流れを把握する仕組みに一緒に取り組む仲間を募集しております。
詳しくは、採用ページをご覧ください。

好きな場所で働こう