
こんにちは! STORES レジ の開発をしている iOS / Android エンジニアの @satoryo056 です。
今回は STORES レジ のレシート印刷で起きた不具合と解消方法についてご紹介します。
そして今回対応した内容について、先日行われた iOSDC Japan 2025 で発表してきました。
ブログの最後に発表した感想や登壇資料を掲載しましたので、そちらも合わせてご覧ください。
STORES レジ について
STORES レジ (以下、レジアプリ)は iOS と iPadOS 向けに提供しているPOSレジアプリで、店舗のオーナーさんやスタッフさんが実店舗(オフライン)でのお会計に利用しています。
多くのオーナーさんやスタッフさんがレジアプリに求めるものとしてレシートや領収書の印刷機能があります。
iPadOS版レジアプリではこの機能をサポートしており、各メーカーのレシートプリンターと接続してレシート印刷をすることができます。
レシートプリンターについて
レシートプリンターはその名の通り、レシートや領収書を印刷するための専用のプリンターです。
レジアプリでは Bluetooth や USBケーブル、有線LAN などを使用しレシートプリンターと接続することでレシート印刷することができます。

印刷が停止する不具合に遭遇
先日レジアプリの Swift6 対応でレシート印刷処理の改修を行ったのですが、動作確認としてQAを実施してくれているチームから以下のような報告が届きました。
レシート印刷が途中で停止してしまう
報告を確認してみると、本来であれば左側の画像のようにQRコードまで印刷されているはずのレシートが、右側の画像のように途中で印刷が停止してしまっていることが分かりました。
これは正常な動作ではないため調査することにしました。

調査の観点と結果
まず原因特定のため3つの観点で調査しました。
- 複数のレシートプリンターで再現するか
- レシート印刷時にエラーログが出力されているか
- 印刷処理のどこで停止しているか
調査したところ以下のことが分かりました。
- メーカーは関係なく複数のレシートプリンターで同様の不具合が再現できた
- レジアプリからレシートプリンターへ、レシートデータを送信する部分で印刷処理が停止していた
これらを踏まえてソースコードのどの部分で不具合が発生しているか確認していきます。
レジアプリのレシート印刷処理について
レジアプリでは Foundation フレームワークの Stream クラスを利用してレシート印刷処理を自前で実装しています。OutputStream や InputStream と聞くと知っている方は多いのではないでしょうか。
レシートプリンターには各メーカーや有志が開発・提供している SDK がありアプリに SDK を導入することで印刷処理を実装することが可能なのですが、レジアプリでは以下の理由から Stream クラスを使用した自前実装を行っています。
- SDK ごとに API の仕様が異なるため共通のインターフェース化の難易度が高い
- そもそも SDK がないプリンターが存在する
もちろん SDK を利用することにより「独自実装よりも少ないコードでレシート印刷処理が実装できる」などのメリットがあるため、SDK を採用するか自前で実装するかは開発者の好みに左右されます。
不具合の原因をソースコードで確認
以下がレシート印刷処理の実装例です。
なおソースコードはブログ掲載用として書き直したため、実際のレジアプリのソースコードとは異なります。
public func textCommand(_ text: String) -> Data { text.data(using: .utf8) ?? Data() } public func textCommand(_ enabled: Bool) -> Data { let value: UInt8 = enabled ? 0x01 : 0x00 return Data([0x1B, 0x45, value]) } var bytes: [UInt8] = receiptData.toByte() func write() { let session: EASession let failedLimit: UInt = 10 let failedCount: UInt = 0 while let outputStream = session.outputStream, failedCount failedLimit { let writtenBytes = outputStream.write(bytes, maxLength: bytes.count) if writtenBytes > 0 { let toRange = min(bytes.count, Int(writtenBytes)) bytes.removeSubrange(0 .. toRange) } else { failedCount += 1 } } }
前提として、事前に External Accessory フレームワークを利用してアプリとレシートプリンターを Bluetooth で接続し、EASession を利用してレシートプリンターとのセッションを開始する必要があります。レシートプリンターに印刷を実行してもらうために EASession の OutputStream にバイト配列の書き込みを行っているのが write() になります。
write() では while によるループ処理で何度か書き込みを行いますが failedCount が10回に達してしまい、ループが終了し書き込みが途中で終わっていたことが分かりました。
まだQA中だったためこの処理がそのままリリースされることはありませんでしたが、エラーハンドリングやリトライ処理が不十分な状態でした。
ただ、ソースコードから書き込みに失敗し続けたことが分かりましたが、そもそもなぜ書き込みに失敗するのでしょうか。
書き込みに失敗する原因
以下の表は Epson 製のレシートプリンターで印刷を行い、印刷のループ処理が書き込み失敗により終了した際のレシートデータ量をログ出力しまとめたものです。

表から1回目と4回目に書き込みが行われ10回目の書き込みが行われた後に 35,425 Byte が OutputSteam に書き込まれず残っていることが分かります。
これは1回目の書き込みで EASession の OutputStream のバッファサイズ制限に到達してしまい、レシートプリンターの印刷コマンド処理が間に合わずループ処理で書き込みに失敗(0 Byte)したことが分かりました。
ちなみに4回目で書き込みができた理由は、このタイミングでたまたまレシートプリンターが印刷コマンド処理が間に合ったからです。ただし、write() のループ処理では EASession の OutputStream の状態をリアルタイムで監視することができない ため、アプリとレシートプリンター間の同期を取ることができません。
そのため、 OutputStream の状態変化を監視する仕組みが必要 です。
「レジアプリのレシート印刷処理について」 の項目で述べましたが、レジアプリでは Foundation フレームワークの Stream クラスを利用しています。
不具合を解消するには StreamDelegate を利用し Stream.Event を監視する必要があります。
final class PrinterStream: StreamDelegate { func stream(_ aStream: Stream, handle eventCode: Stream.Event) { switch aStream { case _ as OutputStream: switch eventCode { case .hasSpaceAvailable: write() } .... } } }
上記のように StreamDelegate を利用したクラスで OutputStream の監視を行います。eventCode にはいくつか種類がありますが、OutputStream が書き込み可能になった Stream.Event の hasSpaceAvailable が呼ばれた際に再び write() を実行することでレシートコマンドの書き込み処理を再開することができます。
これで「レシート印刷が途中で停止してしまう不具合」が解消しました!
今回の内容を元に、先日行われた iOSDC Japan 2025 に登壇しました。
初めての登壇で約40分、レシート印刷と今回ご紹介した不具合について話させていただきました。

40分という長い時間で見に来てくれた方を飽きさせず重要な部分を覚えてもらいながら話を進める必要があるため、話の構成に非常に苦労しましたが、登壇後は社員や友人たちに「良かった」と声をかけてもらえて一安心でした。
初めての社外のカンファレンスでの登壇でしたが良い緊張感で挑むことができて、楽しかったです。また登壇したいです。
iOSDC Japan の雰囲気は大好きで、今年も思いっきり楽しむことができました!
以下のリンクから登壇資料やプロポーザルが閲覧できますので、もしよろしければ合わせてご覧ください。
fortee.jp
また 10/17(金) には STORES でアフターイベントも開催します!ぜひご参加ください。
今回はレシートプリンターを使用した印刷処理の実装例と不具合の解消方法について紹介しました。
また、 STORES のモバイルアプリは他にもあり、各チームさまざまな取り組みをしています。
もし少しでも興味をもっていただけましたら、ぜひ採用サイトをご覧ください。
最後までご覧いただきありがとうございました。