これはフェンリル デザインとテクノロジー Advent Calendar2024 9日目の記事です。
はじめに
DataScannerViewControllerはiOS 16以降で利用可能なクラスです。
VisionKitフレームワークによって提供されており、iPhoneのカメラを使ったQRコードやテキストの読み取り機能を実装することができます。
iOSアプリでQRコードの読み取り機能を実装する場合、今まではAVFoundationの複数のクラスを利用するやや複雑な実装が必要でしたが、DataScannerViewControllerを使うことで、とてもシンプルな実装でリッチなUIの作成が可能になります。
この記事では、DataScannerViewControllerの使い方とQRコード読み取り機能の実装例について簡単に紹介します。
Vision、VisionKitとDataScannerViewController
Appleが提供するフレームワークにはVisionKit以外にもVisionがありますが、これらは別のフレームワークです。
Visionは、コンピュータビジョンアルゴリズムを利用した画像処理・動画処理を行うためのフレームワークです。
このフレームワークは機械学習モデルを保持しており、画像内の物体検出や軌跡の追跡などの機能を提供しています。
VisionKitは、カメラを利用した情報の検出や取得した情報を表示するUIを提供するフレームワークです。
おそらくVisionKitの内部ではVisionフレームワークを使った画像処理が実行されているので、Visionの画像処理機能にI/Oの機能を追加したフレームワークがVisionKitだと考えても良さそうです。
Visionの画像処理では機械学習モデルを利用するため、画像処理を行うためにはある程度パワーのあるプロセッサが必要になります。そのため、DataScannerViewControllerは2018年以降のApple Neural Engineを搭載した端末でなければ利用できません。
DataScannerViewControllerのクラスプロパティであるisSupportedでDataScannerViewControllerが利用可能な端末かを判定できるので、スキャンを開始する前にこのプロパティを使って判定する必要があることに注意しましょう。
DataScannerViewControllerを使ってQRコードを読み取る
ここからはDataScannerViewControllerを使って、SwiftUIベースのアプリにできるだけシンプルなコードでQRコード読み取り機能を実装したいと思います。
※カメラ利用のためのプライバシー権限の設定やisSupportedを使った対応デバイスの確認などについては、説明を省略します。
スキャナを起動して画面に表示する
DataScannerViewControllerはUIViewControllerのサブクラスなので、SwiftUIで表示する場合はUIViewControllerRepresentableを利用します。
makeUIViewController()メソッドでDataScannerViewControllerのインスタンスを作成します。
この中でstartScanning()を呼び出して、QRコードのスキャンを開始します。
import SwiftUI
import VisionKit
struct QRCodeScanner: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> DataScannerViewController {
        let dataScannerViewController = DataScannerViewController(
            recognizedDataTypes: [.barcode(symbologies: [.qr])]  // 識別するデータの種類を指定
        )
        try? dataScannerViewController.startScanning()  // スキャンの開始
        return dataScannerViewController
    }
    
    func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {
    }
}
QRコード認識時にハイライトを表示する
DataScannerViewControllerのイニシャライザでisHighlightingEnabled引数にtrueを渡すと、QRコードを認識した際にハイライトが表示されるようになります。
    func makeUIViewController(context: Context) -> DataScannerViewController {
        let dataScannerViewController = DataScannerViewController(
            recognizedDataTypes: [.barcode(symbologies: [.qr])],
            isHighlightingEnabled: true  // 認識された項目の周囲にハイライトを表示する
        )
        try? dataScannerViewController.startScanning()
        return dataScannerViewController
    }
スキャナが認識中のQRコードのデータを取得する
DataScannerViewControllerがQRコードを認識した時のイベントは、DataScannerViewControllerDelegateでハンドリングします。
スキャナがアイテムを認識した際はdataScanner(_:didAdd:allItems:)が呼び出され、アイテムの認識が外れた時にはdataScanner(_:didRemove:allItems:)が呼び出されます。
struct QRCodeScanner: UIViewControllerRepresentable {
    @Binding var recognizedPayload: String
    func makeUIViewController(context: Context) -> DataScannerViewController {
        let dataScannerViewController = DataScannerViewController(
            recognizedDataTypes: [.barcode(symbologies: [.qr])],
            isHighlightingEnabled: true
        )
        dataScannerViewController.delegate = context.coordinator  // Delegate を設定
        try? dataScannerViewController.startScanning()
        return dataScannerViewController
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    final class Coordinator: NSObject, DataScannerViewControllerDelegate {
        private let parent: QRCodeScanner
        init(_ qrCodeScanner: QRCodeScanner) {
            self.parent = qrCodeScanner
        }
        // スキャナがアイテムの認識を開始すると呼ばれる
        func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {
            guard case .barcode(let barcode) = addedItems.first else {
                return
            }
            if let payloadStringValue = barcode.payloadStringValue {
                parent.recognizedPayload = payloadStringValue
            }
        }
        // スキャナがアイテムの認識を停止した時に呼ばれる
        func dataScanner(_ dataScanner: DataScannerViewController, didRemove removedItems: [RecognizedItem], allItems: [RecognizedItem]) {
            parent.recognizedPayload = ""
        }
    }
}
QRコードをタップした時に処理を実行する
ユーザーが認識中のQRコードをタップすると、DataScannerViewControllerDelegateのdataScanner(_:didTapOn:)が呼び出されます。
これを利用して、「QRコードをタップしたらブラウザでWebページを開く」などの動作を実装できます。
        // スキャナが認識したアイテムをユーザーがタップすると呼ばれる
        func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
            guard case .barcode(let barcode) = item else {
                return
            }
            if let payloadStringValue = barcode.payloadStringValue,
               let url = URL(string: payloadStringValue)
            {
                UIApplication.shared.open(url)
            }
        }
QRコード読み取りの処理を、少ないコードでとてもシンプルに実装できました 🎉
今回作成したQRCodeScannerの最終的な状態は以下のようになります。
import SwiftUI
import VisionKit
struct QRCodeScanner: UIViewControllerRepresentable {
    @Binding var recognizedPayload: String
    func makeUIViewController(context: Context) -> DataScannerViewController {
        let dataScannerViewController = DataScannerViewController(
            recognizedDataTypes: [.barcode(symbologies: [.qr])],
            isHighlightingEnabled: true
        )
        dataScannerViewController.delegate = context.coordinator
        try? dataScannerViewController.startScanning()
        return dataScannerViewController
    }
    
    func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    final class Coordinator: NSObject, DataScannerViewControllerDelegate {
        private let parent: QRCodeScanner
        init(_ qrCodeScanner: QRCodeScanner) {
            self.parent = qrCodeScanner
        }
        func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {
            guard case .barcode(let barcode) = addedItems.first else {
                return
            }
            if let payloadStringValue = barcode.payloadStringValue {
                parent.recognizedPayload = payloadStringValue
            }
        }
        func dataScanner(_ dataScanner: DataScannerViewController, didRemove removedItems: [RecognizedItem], allItems: [RecognizedItem]) {
            parent.recognizedPayload = ""
        }
        func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
            guard case .barcode(let barcode) = item else {
                return
            }
            if let payloadStringValue = barcode.payloadStringValue,
               let url = URL(string: payloadStringValue)
            {
                UIApplication.shared.open(url)
            }
        }
    }
}
参考資料
おまけ
iOSDC Japan 2024で、DataScannerViewControllerについて発表をしました。
YouTubeには発表のアーカイブも残っているので、よければこちらもご覧ください。





