DataScannerViewControllerを使ってiOSアプリ内でのQRコード読み取りをシンプルに実装する

:::note info
これは[フェンリル デザインとテクノロジー Advent Calendar2024](https://adventar.org/calendars/10935) 9日目の記事です。
:::

# はじめに

[DataScannerViewController](https://developer.apple.com/documentation/visionkit/datascannerviewcontroller)はiOS 16以降で利用可能なクラスです。   
VisionKitフレームワークによって提供されており、iPhoneのカメラを使ったQRコードやテキストの読み取り機能を実装することができます。


iOSアプリでQRコードの読み取り機能を実装する場合、今まではAVFoundationの複数のクラスを利用するやや複雑な実装が必要でしたが、DataScannerViewControllerを使うことで、とてもシンプルな実装でリッチなUIの作成が可能になります。     
この記事では、DataScannerViewControllerの使い方とQRコード読み取り機能の実装例について簡単に紹介します。



# Vision、VisionKitとDataScannerViewController

Appleが提供するフレームワークには**VisionKit**以外にも**Vision**がありますが、これらは別のフレームワークです。

[Vision](https://developer.apple.com/documentation/vision/)は、コンピュータビジョンアルゴリズムを利用した画像処理・動画処理を行うためのフレームワークです。   
このフレームワークは機械学習モデルを保持しており、画像内の物体検出や軌跡の追跡などの機能を提供しています。

[VisionKit](https://developer.apple.com/documentation/visionkit)は、カメラを利用した情報の検出や取得した情報を表示するUIを提供するフレームワークです。   
おそらくVisionKitの内部ではVisionフレームワークを使った画像処理が実行されているので、Visionの画像処理機能にI/Oの機能を追加したフレームワークがVisionKitだと考えても良さそうです。

Visionの画像処理では機械学習モデルを利用するため、画像処理を行うためにはある程度パワーのあるプロセッサが必要になります。そのため、DataScannerViewControllerは2018年以降のApple Neural Engineを搭載した端末でなければ利用できません。    
DataScannerViewControllerのクラスプロパティである[`isSupported`](https://developer.apple.com/documentation/visionkit/datascannerviewcontroller/issupported)でDataScannerViewControllerが利用可能な端末かを判定できるので、スキャンを開始する前にこのプロパティを使って判定する必要があることに注意しましょう。

<details><summary>WWDC22のセッションで言及されている箇所</summary>

![WWDC22での、DataScannerViewControllerが対応しているデバイスについての説明](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555559/70156b6b-792b-c6c1-a98e-4d029b427e12.png)

（[VisionKitで機械可読コードやテキストをキャプチャする - WWDC22 - ビデオ - Apple Developer](https://developer.apple.com/jp/videos/play/wwdc2022/10025/)より）

</details>


# DataScannerViewControllerを使ってQRコードを読み取る

ここからはDataScannerViewControllerを使って、SwiftUIベースのアプリにできるだけシンプルなコードでQRコード読み取り機能を実装したいと思います。

※カメラ利用のためのプライバシー権限の設定や`isSupported`を使った対応デバイスの確認などについては、説明を省略します。


## スキャナを起動して画面に表示する

DataScannerViewControllerはUIViewControllerのサブクラスなので、SwiftUIで表示する場合はUIViewControllerRepresentableを利用します。   

`makeUIViewController()`メソッドでDataScannerViewControllerのインスタンスを作成します。    
この中で[`startScanning()`](https://developer.apple.com/documentation/visionkit/datascannerviewcontroller/startscanning())を呼び出して、QRコードのスキャンを開始します。

```swift
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) {
    }
}
```

<details><summary>実行例</summary>

![スキャナを起動して画面に表示するデモ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555559/8ebed57e-727c-bd04-30da-85c304e389f2.gif)

</details>

## QRコード認識時にハイライトを表示する

DataScannerViewControllerのイニシャライザで`isHighlightingEnabled`引数に`true`を渡すと、QRコードを認識した際にハイライトが表示されるようになります。

```swift
    func makeUIViewController(context: Context) -> DataScannerViewController {
        let dataScannerViewController = DataScannerViewController(
            recognizedDataTypes: [.barcode(symbologies: [.qr])],
            isHighlightingEnabled: true  // 認識された項目の周囲にハイライトを表示する
        )
        try? dataScannerViewController.startScanning()
        return dataScannerViewController
    }
```

<details><summary>実行例</summary>

![QRコード認識時にハイライトを表示するデモ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555559/f3a6a0b5-d7c7-b47d-c60d-e3671c1acd5a.gif)

</details>

## スキャナが認識中のQRコードのデータを取得する

DataScannerViewControllerがQRコードを認識した時のイベントは、[DataScannerViewControllerDelegate](https://developer.apple.com/documentation/visionkit/datascannerviewcontrollerdelegate)でハンドリングします。
スキャナがアイテムを認識した際は[`dataScanner(_:didAdd:allItems:)`](https://developer.apple.com/documentation/visionkit/datascannerviewcontrollerdelegate/datascanner(_:didadd:allitems:))が呼び出され、アイテムの認識が外れた時には[`dataScanner(_:didRemove:allItems:)`](https://developer.apple.com/documentation/visionkit/datascannerviewcontrollerdelegate/datascanner(_:didremove:allitems:))が呼び出されます。

```swift
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 = ""
        }
    }
}
```

<details><summary>実行例</summary>

![スキャナが認識中のQRコードのデータを取得するデモ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555559/1aa7d218-d43e-7d88-f25e-5bf980d16443.gif)

</details>

## QRコードをタップした時に処理を実行する

ユーザーが認識中のQRコードをタップすると、DataScannerViewControllerDelegateの[`dataScanner(_:didTapOn:)`](https://developer.apple.com/documentation/visionkit/datascannerviewcontrollerdelegate/datascanner(_:didtapon:))が呼び出されます。   
これを利用して、「QRコードをタップしたらブラウザでWebページを開く」などの動作を実装できます。

```swift
        // スキャナが認識したアイテムをユーザーがタップすると呼ばれる
        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)
            }
        }
```

<details><summary>実行例</summary>

![QRコードをタップした時に処理を実行するデモ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3555559/00734cee-ab5e-58ae-aad1-aa12e06ded94.gif)

</details>

---

QRコード読み取りの処理を、少ないコードでとてもシンプルに実装できました 🎉     
今回作成したQRCodeScannerの最終的な状態は以下のようになります。

```swift
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)
            }
        }
    }
}
```


# 参考資料

https://developer.apple.com/jp/videos/play/wwdc2022/10025/

https://developer.apple.com/documentation/visionkit/scanning-data-with-the-camera


# おまけ

[iOSDC Japan 2024](https://iosdc.jp/2024/)で、DataScannerViewControllerについて発表をしました。
YouTubeには発表のアーカイブも残っているので、よければこちらもご覧ください。

https://fortee.jp/iosdc-japan-2024/proposal/737bea3f-e4ce-4358-81d2-a96177d56004