お店の機器とシステムをつなぐ仕事をしています。プリンターまわりの開発をしていると、お店の方から「突然動かなくなった」「故障なのか分からない」という相談をよく受けます。
アプリから印刷するとき、どこまで届いていて、どこで止まっているのかが分かるように作っておくと、こうしたトラブルにかなり強くなります。
この記事では、スター精密が公開している StarXpand SDK for iOS を使って、iOS アプリから
- プリンターを探す
- 印刷データを作る
- 送って印刷する
- 状態(紙切れ・カバー開き)を取る
- SwiftUI の画面から使う
までを最短でつなぐ流れをまとめます。
この記事の内容は、公開されている README・サンプルアプリ・オンラインマニュアルの範囲です。対応機種や細かい仕様は、必ず公式のマニュアルで確認してください。
StarXpand SDK とは
スター精密のプリンターなどを iOS アプリから扱うための SDK で、中身は StarIO10 というフレームワークです。
- 接続方式:LAN/Bluetooth/Bluetooth Low Energy/USB
- 動作環境:iOS 15.0 以降・Swift 5
- 導入:Swift Package Manager
LAN でも Bluetooth でも、同じ書き方で扱えるのがいちばんの利点だと感じています。
導入
Xcode の File > Add Packages に、次の URL を入れます。
https://github.com/star-micronics/StarXpand-SDK-iOS
Info.plist に足すもの
接続方式によって、必要な設定が変わります。ここを忘れると「見つからない」「つながらない」の原因になります。
| 接続方式 | 必要な設定 |
|---|---|
| LAN | Privacy - Local Network Usage Description |
| Bluetooth / BLE | Privacy - Bluetooth Always Usage Description |
| Bluetooth / USB | Supported external accessory protocols に jp.star-m.starpro
|
⚠️ Bluetooth と USB は MFi の扱いになるので、App Store に出すときは申請の手続きも必要です(詳しくは公式の README を参照してください)。
1. プリンターを探す
StarDeviceDiscoveryManagerFactory で探索用のマネージャーを作り、見つかったプリンターをデリゲートで受け取ります。
import StarIO10
final class PrinterFinder: StarDeviceDiscoveryManagerDelegate {
private var manager: (any StarDeviceDiscoveryManager)?
func start() throws {
manager = try StarDeviceDiscoveryManagerFactory.create(interfaceTypes: [.lan, .bluetooth])
manager?.discoveryTime = 5_000 // ミリ秒
manager?.delegate = self
try manager?.startDiscovery()
}
func manager(_ manager: any StarDeviceDiscoveryManager, didFind printer: StarPrinter) {
let settings = printer.connectionSettings
print("見つかった:", settings.interfaceType, settings.identifier, printer.information?.model as Any)
}
func managerDidFinishDiscovery(_ manager: any StarDeviceDiscoveryManager) {
print("探索おわり")
}
}
見つかったプリンターの connectionSettings(接続方式と識別子)を保存しておけば、次回からは探さずにつなげます。
2. 印刷データを作る
印刷データは、ビルダーを入れ子にして組み立てる形です。
func makeReceipt() -> String {
let builder = StarXpandCommand.StarXpandCommandBuilder()
_ = builder.addDocument(
StarXpandCommand.DocumentBuilder()
.addPrinter(
StarXpandCommand.PrinterBuilder()
.styleAlignment(.center)
.actionPrintText("MonoUni Coffee\n")
.styleAlignment(.left)
.actionPrintText("Coffee 500\n")
.actionPrintText("Total 500\n")
.actionCut(.partial)
)
)
return builder.getCommands()
}
getCommands() が返すのはただの文字列です。作る処理と送る処理が分かれているので、印刷データを作る部分だけをテストしやすいのも嬉しいところです。
⚠️ 画像でしか印字できない機種もあります(サンプルのコメントでは TSP100III シリーズなど)。その場合は actionPrintText の代わりに actionPrintImage を使います。
3. 送って印刷する
StarPrinter に接続設定を渡して、開く → 送る → 閉じる の順です。
func printReceipt(settings: StarConnectionSettings) async {
let printer = StarPrinter(settings)
do {
try await printer.open()
defer { Task { await printer.close() } }
try await printer.print(command: makeReceipt())
print("印刷 OK")
} catch {
print("印刷できなかった:", error)
}
}
// LAN の場合の例(identifier は検出で得た値)
let settings = StarConnectionSettings(interfaceType: .lan, identifier: "00:11:62:XX:XX:XX")
open() で失敗するのか、print() で失敗するのかで、原因の当たりがつけやすくなります。
-
open()で失敗 → つながっていない(電源・ネットワーク・Bluetooth のペアリング・別のアプリが使っている など) -
print()で失敗 → つながってはいるが、印刷できない状態(紙切れ・カバー開き など)
4. 状態を取る
印刷の前後で getStatus() を呼ぶと、プリンターの状態が取れます。
let printer = StarPrinter(settings)
try await printer.open()
let status = try await printer.getStatus()
print("エラー:", status.hasError, "紙切れ:", status.paperEmpty, "カバー開き:", status.coverOpen)
await printer.close()
お店の方に「紙が切れています」「ふたが開いています」と具体的に出してあげるだけで、「壊れた?」という問い合わせはぐっと減ります。
状態の変化を待ち受けたい場合は、printer.printerDelegate を設定すると、紙切れ・カバー開き・通信エラーなどをイベントで受け取れます(サンプルアプリの MonitorView が参考になります)。
5. SwiftUI で使う
SwiftUI では、ここまでの処理を ViewModel にまとめて、画面は「探す」「選んで印刷」「結果を出す」だけにしておくと扱いやすいです(Swift 5 モードを前提にしています)。
import SwiftUI
import StarIO10
struct FoundPrinter: Identifiable {
let id = UUID()
let settings: StarConnectionSettings
let label: String
}
@MainActor
final class PrinterViewModel: ObservableObject, StarDeviceDiscoveryManagerDelegate {
@Published var printers: [FoundPrinter] = []
@Published var message: String?
private var manager: (any StarDeviceDiscoveryManager)?
func discover() {
printers = []
do {
manager?.stopDiscovery()
manager = try StarDeviceDiscoveryManagerFactory.create(interfaceTypes: [.lan, .bluetooth])
manager?.discoveryTime = 5_000
manager?.delegate = self
try manager?.startDiscovery()
message = "探しています…"
} catch {
message = "探せませんでした: \(error)"
}
}
// SDK からの通知はメインスレッドとは限らないので、受け取ってから MainActor へ戻す
nonisolated func manager(_ manager: any StarDeviceDiscoveryManager, didFind printer: StarPrinter) {
let settings = printer.connectionSettings
let label = "\(printer.information?.model, default: "") \(settings.identifier)"
Task { @MainActor in
self.printers.append(FoundPrinter(settings: settings, label: label))
}
}
nonisolated func managerDidFinishDiscovery(_ manager: any StarDeviceDiscoveryManager) {
Task { @MainActor in
self.message = self.printers.isEmpty ? "見つかりませんでした" : nil
}
}
func printReceipt(_ settings: StarConnectionSettings) async {
let printer = StarPrinter(settings)
do {
try await printer.open()
} catch {
message = "プリンターにつながりませんでした"
return
}
defer { Task { await printer.close() } }
do {
try await printer.print(command: makeReceipt())
message = "印刷しました"
} catch {
// 失敗したら状態を取りにいき、お店の人に分かる言葉で出す
let status = try? await printer.getStatus()
if status?.paperEmpty == true {
message = "紙が切れています"
} else if status?.coverOpen == true {
message = "ふたが開いています"
} else {
message = "印刷できませんでした: \(error)"
}
}
}
}
struct PrinterView: View {
@StateObject private var viewModel = PrinterViewModel()
var body: some View {
List {
Button("プリンターを探す") { viewModel.discover() }
Section("見つかったプリンター") {
ForEach(viewModel.printers) { printer in
Button(printer.label) {
Task { await viewModel.printReceipt(printer.settings) }
}
}
}
if let message = viewModel.message {
Text(message).foregroundStyle(.secondary)
}
}
}
}
ポイントは 2 つです。
-
SDK のデリゲートは
nonisolatedで受けて、@MainActorに戻してから画面の値を書き換える(公式サンプルの DiscoveryView も同じ形です) -
印刷に失敗したら
getStatus()を取りにいき、「紙が切れています」のように言い換えて出す。エラーをそのまま見せないだけで、現場の安心感がかなり違います
makeReceipt() は「2. 印刷データを作る」の関数をそのまま使っています。
まとめ
- 導入は SPM、Info.plist の設定は接続方式ごとに忘れずに
- 探す → 作る → 送る → 状態を見る の 4 つに分けて書くと、トラブルの切り分けがしやすい
- 画面に状態を具体的に出すと、お店の方の「壊れた?」が減る
- SwiftUI では ViewModel にまとめ、デリゲートは MainActor に戻してから画面を更新する
公式のサンプルアプリには、レシートやラベルの印刷例がたくさん入っています。まずはサンプルを動かしてみるのがおすすめです。