WWDC Quick Look 💓 By SwiftGGTeam
Capture machine-readable codes and text with VisionKit

Capture machine-readable codes and text with VisionKit

元の動画を見る

ハイライト

iOS でのライブデータスキャンは、これまで AVFoundation の metadata output(機械コードのみ)か、AVFoundation と Vision の組み合わせ(コード量が多く座標変換が面倒)でした。iOS 16 の DataScannerViewController はこれらをすべてすぐ使える ViewController にまとめます。


主要内容

スキャン UI を作るには、従来2つの道がありました。

QR コードとバーコードだけなら AVFoundation で capture session に入出力を接続し、AVMetadataObject を取得します。文字も必要なら AVFoundation と Vision を接続します。カメラが sample buffer を出力し、Vision が文字またはバーコード認識リクエストを実行し、observation を UI にマッピングし直します。

難しいのは認識そのものではありません。ライブカメラプレビュー、ユーザー許可、タップフォーカス、ピンチズーム、認識項目のハイライト、image space から Vision 座標、さらに view 座標への変換です。

DataScannerViewController は iOS 16 の選択肢です。VisionKit 内で AVFoundation と Vision を UIViewController サブクラスに封装します。認識タイプを指定し、present して startScanning() を呼びます。カメラプレビュー、ガイダンス文字、システムハイライト、タップフォーカス、ズームはすべてシステムが処理します。

向いているシーンは明確です。倉庫ピッキング、レジ、伝票入力、現場棚卸です。これら App の主作業はスキャン後の業務フローです。Data Scanner はスキャン部分を短くします。


詳細

まずデバイスと権限を確認する

04:01)Data Scanner はすべてのデバイスでサポートされるわけではありません。Apple は講演で、2018年以降で Apple Neural Engine を搭載した iPhone と iPad がサポートすると説明しています。

したがって入口ボタンを常に表示すべきではありません。まず isSupported でハードウェア対応を確認し、次に isAvailable で現在利用可能か確認します。isAvailable はカメラ許可や Screen Time のカメラアクセス制限などデバイス制限の影響を受けます。

import VisionKit

if DataScannerViewController.isSupported,
   DataScannerViewController.isAvailable {
    showScanButton()
} else {
    hideScanButton()
}

キーポイント:

  • import VisionKitDataScannerViewController を導入します。
  • isSupported はデバイスハードウェアがライブデータスキャンをサポートするかを判断します。
  • isAvailable はカメラ権限とシステム制限を含む現在状態でスキャン可能かを判断します。
  • 条件を満たさないときは入口を隠し、動作しないスキャンフローへの誘導を避けます。

Info.plist に camera usage description も追加します。講演は、なぜカメラが必要かを説明し、ユーザーが何を許可しているか分かるようにするよう注意します。

スキャナーを作成する

04:40)スキャナー作成時、最も重要なパラメータは recognizedDataTypes です。システムに何を探すかを伝えます。

公式例はまず QR コードをスキャンし、文字、言語指定、URL のみに変更する方法も示します。

import VisionKit

// Specify the types of data to recognize
let recognizedDataTypes:Set<DataScannerViewController.RecognizedDataType> = [
    .barcode(symbologies: [.qr]),
    // uncomment to filter on specific languages (e.g., Japanese)
    // .text(languages: ["ja"])
    // uncomment to filter on specific content types (e.g., URLs)
    // .text(textContentType: .URL)
]

// Create the data scanner, present it, and start scanning!
let dataScanner = DataScannerViewController(recognizedDataTypes: recognizedDataTypes)
present(dataScanner, animated: true) {
    try? dataScanner.startScanning()
}

キーポイント:

  • Set<DataScannerViewController.RecognizedDataType> は機械コードと文字の両方を含められます。
  • .barcode(symbologies: [.qr]) は機械コード認識を QR コードに限定します。
  • .text(languages: ["ja"]) は文字認識に言語ヒントを渡せます。講演では日本語と韓国語が iOS 16 Live Text の新対応言語として特に言及されています。
  • .text(textContentType: .URL) は URL など特定意味の文字だけを探せます。
  • DataScannerViewController(recognizedDataTypes:) は通常の view controller で、全画面、sheet、カスタム階層に使えます。
  • presentation 完了後に startScanning() を呼び、ライブ映像からデータを探し始めます。

06:29)初期化パラメータで体験も制御できます。qualityLevelbalancedfastaccurate から選べます。Apple は balanced から始めることを推奨します。大きくはっきりした文字は fast、非常に小さい QR やシリアル番号は accurate が向きます。

let dataScanner = DataScannerViewController(
    recognizedDataTypes: recognizedDataTypes,
    qualityLevel: .balanced,
    recognizesMultipleItems: true,
    isHighFrameRateTrackingEnabled: true,
    isPinchToZoomEnabled: true,
    isGuidanceEnabled: true,
    isHighlightingEnabled: true
)

キーポイント:

  • qualityLevel: .balanced は多くのシーンの出発点です。
  • recognizesMultipleItems: true は1フレーム内の複数項目を許可し、複数バーコード同時スキャンに適します。
  • isHighFrameRateTrackingEnabled はカスタムハイライトを動く対象により密に追従させます。
  • isPinchToZoomEnabled はユーザーがピンチズームできるかを制御します。
  • isGuidanceEnabled は上部ガイダンスラベルを表示し、対象合わせを支援します。
  • isHighlightingEnabled はシステムハイライトを有効化。独自描画するならオフにできます。

ユーザーがタップした項目を処理する

08:11)App がユーザーが何をタップしたか知る必要があるなら delegate を設定します。

// Specify the types of data to recognize
let recognizedDataTypes:Set<DataScannerViewController.RecognizedDataType> = [
    .barcode(symbologies: [.qr]),
    .text(textContentType: .URL)
]

// Create the data scanner, present it, and start scanning!
let dataScanner = DataScannerViewController(recognizedDataTypes: recognizedDataTypes)
dataScanner.delegate = self
present(dataScanner, animated: true) {
    try? dataScanner.startScanning()
}

キーポイント:

  • ここでは QR コードと URL テキストの両方を認識します。
  • dataScanner.delegate = self で現在オブジェクトがスキャナーイベントを受け取ります。
  • delegate はタップ、認識項目の追加、更新、削除などのコールバックを受けます。
  • 画面に載ってからスキャンを開始するため、present 完了後にスキャンを起動します。

08:19)ユーザーが認識項目をタップすると、delegate は RecognizedItem を受け取ります。これは enum で、文字またはバーコードの可能性があります。

func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
    switch item {
    case .text(let text):
        print("text: \(text.transcript)")
    case .barcode(let barcode):
        print("barcode: \(barcode.payloadStringValue ?? "unknown")")
    default:
        print("unexpected item")
    }
}

キーポイント:

  • didTapOn はユーザーが認識項目をタップしたときに発火します。
  • .text(let text) 分岐で文字認識結果を読みます。
  • text.transcript は認識された文字列です。
  • .barcode(let barcode) 分岐で機械コードを処理します。
  • barcode.payloadStringValue は payload が文字列に変換できるときのみ値があり、例は ?? "unknown" でフォールバックします。
  • RecognizedItem には追跡とハイライト描画用の安定 idbounds もあります。

カスタムハイライト

08:54)各 RecognizedItem には bounds があります。講演では、この値は4つの角点で構成されると特に説明しています。透視変換下では文字やコードが台形になるため、通常の矩形として扱わないでください。

システムハイライトが App スタイルに合わない場合、3組の delegate メソッドで独自描画できます。

09:11)認識項目が現れた直後、ハイライト view を作成し overlayContainerView に追加します。

// Dictionary to store our custom highlights keyed by their associated item ID.
var itemHighlightViews: [RecognizedItem.ID: HighlightView] = [:]

// For each new item, create a new highlight view and add it to the view hierarchy.
func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {
    for item in addedItems {
        let newView = newHighlightView(forItem: item)
        itemHighlightViews[item.id] = newView
        dataScanner.overlayContainerView.addSubview(newView)
    }
}

キーポイント:

  • itemHighlightViewsRecognizedItem.ID を key に認識項目とカスタムハイライトを結び付けます。
  • didAdd は新項目が認識されたときに呼ばれます。
  • newHighlightView(forItem:) は認識項目用のハイライト view を作成します。
  • item.id は画面から消えるまで項目ライフサイクル内で安定します。
  • overlayContainerView はカメラプレビュー上にあり、カスタムハイライトの載せ先に適しています。

09:37)項目やカメラが動いたら、ハイライト位置を更新します。

// Animate highlight views to their new bounds
func dataScanner(_ dataScanner: DataScannerViewController, didUpdate updatedItems: [RecognizedItem], allItems: [RecognizedItem]) {
    for item in updatedItems {
        if let view = itemHighlightViews[item.id] {
            animate(view: view, toNewBounds: item.bounds)
        }
    }
}

キーポイント:

  • didUpdate は項目移動、カメラ移動、文字転写結果変化時に呼ばれます。
  • updatedItems は変化した項目のみを含みます。
  • item.id で既存ハイライト view を取り戻します。
  • item.bounds は新しい4角座標で、ハイライトアニメーションの駆動に使います。

10:03)項目が画面から消えたら、対応ハイライトを削除します。

// Remove highlights when their associated items are removed.
func dataScanner(_ dataScanner: DataScannerViewController, didRemove removedItems: [RecognizedItem], allItems: [RecognizedItem]) {
    for item in removedItems {
        if let view = itemHighlightViews[item.id] {
            itemHighlightViews.removeValue(forKey: item.id)
            view.removeFromSuperview()
        }
    }
}

キーポイント:

  • didRemove は項目が見えなくなったときに呼ばれます。
  • removedItems はシーンから去った認識項目を示します。
  • removeValue(forKey:) で辞書参照を消します。
  • removeFromSuperview() で UI からハイライトを外します。
  • allItems は現在認識中の全項目を含みます。文字認識では自然な読み順に並びます。

写真撮影と AsyncStream

10:54)スキャナーは高品質な静止画も撮れます。例はカメラロールへ保存します。

// Take a still photo and save to the camera roll
if let image = try? await dataScanner.capturePhoto() {
    UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}

キーポイント:

  • capturePhoto() は非同期メソッドです。
  • 成功時は UIImage を返します。
  • 例は UIImageWriteToSavedPhotosAlbum でシステム写真ライブラリへ保存します。
  • スキャン結果と現場写真を一緒に保管する用途に適し、伝票、棚、機器銘板などです。

11:10)カスタムハイライトが不要なら、3組の delegate メソッドは使わなくても構いません。recognizedItems は継続更新される AsyncStream を提供します。

// Send a notification when the recognized items change.
var currentItems: [RecognizedItem] = []

func updateViaAsyncStream() async {
    guard let scanner = dataScannerViewController else { return }

    let stream = scanner.recognizedItems
    for await newItems: [RecognizedItem] in stream {
        let diff = newItems.difference(from: currentItems) { a, b in
            return a.id == b.id
        }

        if !diff.isEmpty {
            currentItems = newItems
            sendDidChangeNotification()
        }
    }
}

キーポイント:

  • currentItems は前回の認識結果を保持します。
  • dataScannerViewController がないときは即 return します。
  • scanner.recognizedItems[RecognizedItem] を継続出力する async stream です。
  • for await でシーン変化に応じて新配列を受け取ります。
  • difference(from:)id で項目同一性を比較します。
  • diff が空でないときだけ状態更新と通知送信を行います。

重要ポイント

1. 倉庫ピッキングスキャナー

  • 何をするか: 棚上の複数 QR コードまたはバーコードを一度に認識し、商品をタップしたらピッキングフローへ入る。
  • なぜ価値があるか: recognizesMultipleItems は同一画面内の複数認識項目をサポートし、didTapOn でユーザーが選んだ項目を取得できます。
  • 始め方: recognizedDataTypes.barcode(...) に設定し、複数項目認識を有効化。barcode.payloadStringValue で在庫システムの SKU と照合します。

2. 伝票の URL と電話番号抽出

  • 何をするか: カメラで紙の伝票やポスターをスキャンし、URL または電話番号だけを抽出する。
  • なぜ価値があるか: Data Scanner はテキスト content type フィルタをサポートし、無関係な文字が業務フローに入るのを減らせます。
  • 始め方: .text(textContentType: .URL) または対応する意味タイプを使い、ユーザーが認識項目をタップした後 text.transcript を読みます。

3. 現場資産棚卸

  • 何をするか: 機器銘板の小さなシリアル番号をスキャンし、現場写真も記録として残す。
  • なぜ価値があるか: qualityLevel: .accurate は小項目に適し、capturePhoto() で高品質静止画を保存できます。
  • 始め方: 初期化で .accurate を選択。シリアル番号認識後に await dataScanner.capturePhoto() を呼び、画像と認識テキストを一緒に記録します。

4. ブランド化スキャン画面

  • 何をするか: システムハイライトをオフにし、独自の色、アニメーション、形状で認識項目を示す。
  • なぜ価値があるか: overlayContainerViewdidAdddidUpdatedidRemove で完全なライフサイクルを提供します。
  • 始め方: RecognizedItem.ID でハイライト辞書を維持し、didUpdateitem.bounds に基づき4角位置を更新します。

5. リアルタイムテキスト看板

  • 何をするか: カメラが今見ている文字を横のリストやデバッグパネルへリアルタイム集約する。
  • なぜ価値があるか: recognizedItemsAsyncStream で、認識変化を Swift Concurrency ワークフローへ接続しやすいです。
  • 始め方: task 内で for await により scanner.recognizedItems を読み、difference(from:) で変化のないフレームを除外します。

関連セッション

  • Add Live Text interaction to your app — Data Scanner はライブカメラストリームを処理。本 Session は静止画像と一時停止動画フレームの Live Text 操作を扱います。
  • Extract document data using Vision — Data Scanner は一般的なライブスキャンフローを封装。本 Session は Vision を直接使った文書文字とバーコード認識を示します。
  • Create camera extensions with Core Media IO — カメラ入力パイプライン自体に関心があるなら、macOS カメラシステム拡張を扱う Session です。
  • Discover PhotoKit change history — スキャン結果は写真や画像ワークフローに落ちることが多く、写真ライブラリ変化の追跡を扱う Session です。

コメント

GitHub Issues · utterances