🍁

Swift: VisionKitでOCRやQRコードのスキャンをサクッと実装

に公開

ボタンを押したらスキャン用のシートが開き、テキストを読み取ったりQRコードを読み取ったりできるようにします。


サンプル:テキストフィールドにフォーカスするとスキャン用のボタンが表示される


サンプル:テキストをスキャンして文字列がテキストフィールドに反映される


サンプル:QRコードをスキャンしてURLがテキストフィールドに反映される

まず、カメラを使うので権限に対する説明をCustom iOS Target Propertiesに追加しておく必要があります。

Key Value
Privacy - Camera Usage Description For scanning texts or QR codes.

次に、VisionKitのDataScannerViewControllerを使ってUIViewControllerRepresentableを実装してSwiftUIで扱えるようにします。

import SwiftUI
import VisionKit

private struct Scanner: UIViewControllerRepresentable {
    @Environment(\.dismiss) var dismiss
    @Binding var value: String?
    var type: ScanningType

    func makeUIViewController(context: Context) -> DataScannerViewController {
        // ここでスキャナーの仕様をいろいろ設定する
        let controller = DataScannerViewController(
            recognizedDataTypes: [type.dataType],
            qualityLevel: .accurate,
            recognizesMultipleItems: false,
            isHighFrameRateTrackingEnabled: false,
            isHighlightingEnabled: true
        )
        controller.delegate = context.coordinator
        try? controller.startScanning()
        return controller
    }

    func updateUIViewController(
        _ controller: DataScannerViewController,
        context: Context
    ) {}

    static func dismantleUIViewController(
        _ controller: DataScannerViewController,
        coordinator: Coordinator
    ) {
        controller.stopScanning()
    }

    func makeCoordinator() -> Coordinator {
        Coordinator { value in
            self.value = value
            dismiss()
        }
    }

    final class Coordinator: NSObject, DataScannerViewControllerDelegate {
        private let onScan: (String?) -> Void

        init(onScan: @escaping (String?) -> Void) {
            self.onScan = onScan
        }

        // 認識した対象物をタップした時に呼び出されるdelegate関数
        func dataScanner(
            _ dataScanner: DataScannerViewController,
            didTapOn item: RecognizedItem
        ) {
            switch item {
            case let .barcode(value):
                onScan(value.payloadStringValue)
            case let .text(value):
                onScan(value.transcript)
            @unknown default:
                break
            }
        }
    }
}

// 今回はテキストとQRコードの両方に対応するため、Type Safeを意識してenumにした
enum ScanningType {
    case text
    case qrCode

    var dataType: DataScannerViewController.RecognizedDataType {
        switch self {
            case .text:
            return .text(languages: ["ja"])
        case .qrCode:
            return .barcode(symbologies: [.qr])
        }
    }
}

SwiftUIのシートで、かつModifier形式で呼び出せるようにします。

extension View {
    func scanner(isPresented: Binding<Bool>, value: Binding<String?>, type: ScanningType) -> some View {
        sheet(isPresented: isPresented) {
            Scanner(value: value, type: type)
        }
    }
}

最後に作ったUIViewControllerRepresentableを使います。キーボードの上にボタンを配置したかったので、ToolbarItemGroup(placement: .keyboard)を使いました。

import SwiftUI

struct ContentView: View {
    @State var text = ""
    @State var showingScanner = false
    @State var scanningType = ScanningType.text

    var body: some View {
        VStack {
            TextField(text: $text, axis: .vertical) {
                EmptyView()
            }
            .textFieldStyle(.roundedBorder)
            .labelsHidden()
            .lineLimit(10, reservesSpace: true)
            Spacer()
        }
        .padding()
        .toolbar {
            ToolbarItemGroup(placement: .keyboard) {
                Spacer()
                Button {
                    scanningType = .text
                    showingScanner = true
                } label: {
                    Image(systemName: "text.viewfinder")
                }
                Button {
                    scanningType = .qrCode
                    showingScanner = true
                } label: {
                    Image(systemName: "qrcode.viewfinder")
                }
            }
        }
        .scanner(
            isPresented: $showingScanner,
            value: Binding<String?>(get: { nil }, set: { text += $0 ?? "" }),
            type: scanningType
        )
    }
}

VisionKitだとめっちゃ実装簡単ですね。

Discussion