WWDC22、iOS16：iOSアプリに画像からテキストを選択する機能を追加（VisionKit）

:::note
※一般公開されているWWDC Keynoteの動画と公開Session/Documentation/Sample Codeページだけを使ってこの記事を執筆しました。
:::

iOS 16でVisionKitフレームワークを使用することで、あなたのアプリはわずか数行のコードでカメラストリームからリアルタイムにテキストを認識できます（日本語にも対応しています）。

![image-text-extraction.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/38fc0938-09ba-af94-d398-14100ebb3c31.jpeg)

例えば、これはApple WWDCのセッションビデオからのスキャンシーンです。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/935b589e-f037-eb1e-8718-edbaf7622202.gif">

スマホを動かすと、フォーカスしている領域が変化する。
フォーカスされた部分はハイライト表示されます。
そして、ユーザーがフォーカスされた部分をタップすると、認識されたコンテンツがデリゲート(DataScannerViewControllerDelegate)経由で配信されます。

また、機械読み取り可能なコードのみをスキャナーに探させることも可能です。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/60993590-7a6f-e72e-4f2a-b395245f4846.png">

これは、例えばQRコードをスキャンするアプリ機能を構築している場合に特に有効です。

ライブカメラストリームの代わりに、完全なスキャンを行うための画像をAPIに提供することも可能です。
ユーザーは、認識された結果からテキストをコピーすることを選択できます（画像自体にオーバーレイ表示されます）。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/d99f8b84-fbe4-a415-cb1b-3aabf648f5b8.png">

このAPIは主にUIKitで利用できるので、この記事ではまずUIKit (StoryBoard)でこれらの機能を実装し、その後SwiftUI対応のビューを作成することについて話します。

:::note warn
この記事で紹介するフレームワークは、Xcode 14 betaとiOS 16のシミュレータ/デバイスで動作します。
:::

# ライブカメラスキャン

上記の1つ目と2つ目のデモ画像に示すように、ライブカメラストリームを使用して、興味のあるコンテンツをスキャンすることができます。
興味のあるコンテンツを定義する(`recognizedDataTypes`)ことができるので、システムは関連するコンテンツのみをハイライトします。
例えば、すべてのテキストをスキャンするか、電話番号だけをスキャンするかを設定できます。
また、機械読み取り可能なコード（QRコードやバーコードなど）をスキャンするように定義することもできます。

まず、カメラアクセス許可の説明文字列 `NSCameraUsageDescription` を `Info.plist` に追加します。

次に、`VisionKit` フレームワークをインポートします。

データスキャナが利用可能かどうかは `DataScannerViewController.isSupported` と `DataScannerViewController.isAvailable` を使って確認する。

次に、データスキャナ `DataScannerViewController` を初期化することで、スキャンインターフェースを提示する。

```swift
@IBAction func actionShowDataScanner() {
    let scannerVC = DataScannerViewController(
        recognizedDataTypes: [.text()],
            qualityLevel: .fast,
            recognizesMultipleItems: false,
            isHighFrameRateTrackingEnabled: false,
            isGuidanceEnabled: true,
            isHighlightingEnabled: true
    )
    scannerVC.delegate = self
    try? scannerVC.startScanning()
    self.present(scannerVC, animated: true)
}
```

### recognizedDataTypes

これは、認識したいデータの種類を定義します。

`recognizedDataTypes: [.text(languages: ["ja"])]`: 日本語のテキストを認識することを意味する。
他の言語の入力をISOフォーマットで指定することもできます。値を指定しない場合は、システムで設定されているユーザー優先言語が使用されます。

また、スキャンしたいコンテンツの種類を定義することができます。例：`recognizedDataTypes: [.text(languages: ["ja"], textContentType: .telephoneNumber)]` は、電話番号のスキャンを意味します。

その他のコンテンツタイプとしては、日時をスキャンする `dateTimeDuration`、
メールアドレスをスキャンする `emailAddress`、
フライト番号をスキャンする `flightNumber`、
住所をスキャンする `fullStreetAddress`、
郵便物の追跡番号をスキャンする `shipmentTrackingNumber`、
URLをスキャンする `URL` などがあります。

また、機械読み取り可能なコード(バーコードやQRコード)を検索させることもできる。

`recognizedDataTypes: [.barcode()]`

このコードはバーコードを読み取りますが、QRコードも動作することが確認されています。

## `DataScannerViewControllerDelegate`デリゲートの設定とタップされた結果の受信

ユーザーがハイライト部分をタップすると、デリゲート内で `didTapOn` 関数が呼び出されます。ここでは、テキストの内容（transcript、つまり画像から認識された文字列）、またはバーコードのペイロード文字列を読み取ることができます。

```swift
extension ViewController: DataScannerViewControllerDelegate {
    
    func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
        switch item {
        case .text(let text):
            print(text.transcript)
        case .barcode(let barcode):
            print(barcode.payloadStringValue)
        }
    }
    
}
```

### その他の構成

![data_scanner_config.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/5ee6e8a5-8954-fbd9-f109-0c7d59558a6f.jpeg)

| 構成 | 説明 |
|---|---|
| .qualityLevel | スキャナがデータを見つけるために使用する解像度。 |
| .recognizesMultipleItems | スキャナがライブ映像内のすべてのアイテムを識別すべきかどうかを示すbool値。 |
| .isHighFrameRateTrackingEnabled | スキャナが認識したアイテムのジオメトリを更新する頻度を決定するbool値。 |
| .isPinchToZoomEnabled | ユーザーが2本指のピンチインズームジェスチャーを使用できるかどうかを示すbool値。 |
| .isGuidanceEnabled | スキャナが項目を選択する際に、ユーザーにヘルプを提供するかどうかを示すbool値。 |
| .isHighlightingEnabled | スキャナが認識した項目の周囲にハイライトを表示するかどうかを示すbool値。 |

# フルイメージスキャン

ライブカメラストリームの代わりに、静止画像（撮影した写真や保存した画像）をスキャンしてすべてのテキストを調べ、ユーザーがテキストを選択できるようにすることもできます。

そのためには、画像ビューの上に分析結果をオーバーレイする必要があります。そのため、まずUIImageViewを作成する必要があります。この例では、UIStoryBoardを使い、UIImageViewのアウトレットを追加、リンクしています。

```swift
@IBOutlet weak var imageView: UIImageView!
```

次に、画像解析に関連する変数を追加します。

```swift
let analyzer = ImageAnalyzer()
let interaction = ImageAnalysisInteraction()
```

ビューがセットアップされたら、UIImageViewに解析オーバーレイ（interactionと呼びます）を追加します。

```swift
imageView.addInteraction(interaction)
```

新しい画像が手に入ったら、その画像をUIImageViewに更新し、前回の解析結果をクリアして、関数を呼び出して画像解析を実行します。

```swift
// Clear previous analysis
interaction.preferredInteractionTypes = []
interaction.analysis = nil
// Apply new image and start analyzing
self.imageView.image = imageObj
self.analyzeCurrentImage()
```

```swift
func analyzeCurrentImage() {
        guard let image = imageView.image else {
            return
        }
        Task {
            let configuration = ImageAnalyzer.Configuration([.text, .machineReadableCode])
            do {
                let analysis = try await analyzer.analyze(image, configuration: configuration)
                if let analysis = analysis {
                    interaction.analysis = analysis
                    interaction.preferredInteractionTypes = .textSelection
                }
            }
            catch {
                print(error)
            }
        }
    }
```

上記の関数では、テキストと機械可読コードをスキャンする必要があることを定義したコンフィギュレーションを設定します。
解析が完了するのを待つために、`await` キーワードを使用します。
そして、解析結果をinteractionオブジェクトに保存します（これは画像オブジェクトにオーバーレイとして表示されます）。

![IMG_1184.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/5de0b9f6-3522-158b-bd53-5f2eb83a9346.png)

解析が完了すると、画面右下にテキスト変換のアイコンが表示されます。

![scanning-button-visionkit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/8588b437-bf80-7b9c-f40f-41bd6128c981.png)

これで、認識されたテキストエリアがハイライト表示されるはずです。ユーザーは長押しして選択し、コピーすることができます。

`interaction.allowLongPressForDataDetectorsInTextMode`を設定すると、認識結果が出たときに、ユーザーが直接テキストを長押しして選択できるようになります。

また、`interaction.selectableItemsHighlighted`を設定すると、認識プロセスが完了したときに、項目が自動的にハイライトされます。

# SwiftUIとの互換性

私は、`DataScannerViewController`ビューコントローラとして表現可能（SwiftUIでシートとして表示可能）、
`ImageAnalysisInteraction`ビューとして表現可能（SwiftUIで直接表示可能）に
ラップしたライブラリをオープンソースとして公開しています。

https://github.com/mszpro/VisionLiveText_SwiftUICompatible

---


:thumbsup: お読みいただきありがとうございました。

[☺️ Twitter @MszPro](https://twitter.com/MszPro)
[🐘 Mastodon @me@mszpro.com](https://sns.mszpro.com/@me)

:sunny: <img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">

---
![writing-quickly_emoji_400.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/640938e9-c121-8152-b8c4-8ab63124afcc.png)

Written by MszPro~

---

# 関連記事

・[UICollectionViewの行セル、ヘッダー、フッター、またはUITableView内でSwiftUIビューを使用（iOS 16, UIHostingConfiguration）](https://qiita.com/MaShunzhe/items/3d98fa897aa1bcffc74b#github-repository)

・[iPhone 14 ProのDynamic Islandにウィジェットを追加し、Live Activitiesを開始する（iOS16.1以降）](https://qiita.com/MaShunzhe/items/6cc8d9d204f40cbee19c)

・[iOS 16：秘密値の保存、FaceID認証に基づく個人情報の表示/非表示（LARight）](https://qiita.com/MaShunzhe/items/07c1e65277a5008106db)

・[iOS16 MapKitの新機能 : 地図から場所を選ぶ、通りを見回す、検索補完](https://qiita.com/MaShunzhe/items/693e6302876707d5684d)

・[SwiftUIアプリでバックグラウンドタスクの実行(ネットワーク、プッシュ通知) (BackgroundTasks, URLSession)](https://qiita.com/MaShunzhe/items/4e0d1032748fe4f398de)

・[WWDC22、iOS16：iOSアプリに画像からテキストを選択する機能を追加（VisionKit）](https://qiita.com/MaShunzhe/items/ed3985d05975bf35292d)

・[WWDC22、iOS16：数行のコードで作成できるSwiftUIの新機能（26本）](https://qiita.com/MaShunzhe/items/0ed36d8a6de7c56b1d34)

・[WWDC22、iOS 16：SwiftUIでChartsフレームワークを使ってチャートを作成する](https://qiita.com/MaShunzhe/items/cae62af8b07f4f474fd8)

・[WWDC22, iOS 16: WeatherKitで気象データを取得](https://qiita.com/MaShunzhe/items/dce45ccdd05c1f70cf54)

・[WWDC 2022の基調講演のまとめ記事](https://qiita.com/MaShunzhe/items/037580d2ed51a5a6e9d9)