0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

iOSで簡単に書類スキャンする

Last updated at Posted at 2025-04-26

iOSで書類をスキャンしたいと思った時に、なんか小難しいことしないとなのかなと漠然と思いつつ調べていたら、想像以上に簡単にできそうなAPIが見つかりました。
VNDocumentCameraViewControllerが VisionKit フレームワークに iOS13 から登場していて技術調査がてら試しに使ってみたので、使用方法など記事にしてみます。

使い方はめちゃくちゃ簡単で、VNDocumentCameraViewControllerをインスタンス化して、名前の通りUIViewControllerなのでpresentなりpushなりで表示するだけで、以下のような UI を提供してくれます。

書類スキャンでも.gif

注意点として、カメラを使用するので、Info.plist にPrivacy - Camera Usage Descriptionを加えておく必要があります。
これがないと、VNDocumentCameraViewController表示時にクラッシュします。

画像の取得などは、VNDocumentCameraViewControllerDelegateで制御することになります。

具体的な実装内容

import SwiftUI
import VisionKit

struct ScannerView: View {
    
    @Environment(\.dismiss) var dismiss
    @State var scanModel: ScanModel?
    
    var body: some View {
        Scanner { model in
            print("title: \(model.title)")
            scanModel = model
        } onError: { error in
            print("onError: \(error)")
            dismiss()
        } onCancel: {
            print("onCancel")
            dismiss()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .fullScreenCover(item: $scanModel) { model in
            ScanImagePreviewView(previewImage: model.images.first!)
        }
    }
}

struct Scanner: UIViewControllerRepresentable {

    private let onFinish: (ScanModel) -> Void
    private let onCancel: () -> Void
    private let onError: (Error) -> Void

    init(onFinish: @escaping (ScanModel) -> Void,
         onError: @escaping (Error) -> Void,
         onCancel: @escaping () -> Void = {}) {

        self.onFinish = onFinish
        self.onCancel = onCancel
        self.onError = onError
    }

    func makeUIViewController(context: Context) -> VNDocumentCameraViewController {
        let viewController = VNDocumentCameraViewController()
        viewController.delegate = context.coordinator
        return viewController
    }

    func updateUIViewController(_ uiViewController: VNDocumentCameraViewController, context: Context) {}

    func makeCoordinator() -> ScannerCoordinator {

        return ScannerCoordinator(onFinish: onFinish, onCancel: onCancel, onError: onError)
    }
}

final class ScannerCoordinator: NSObject, VNDocumentCameraViewControllerDelegate {

    private let onFinish: (ScanModel) -> Void
    private let onCancel: () -> Void
    private let onError: (Error) -> Void

    init(onFinish: @escaping (ScanModel) -> Void,
         onCancel: @escaping () -> Void,
         onError: @escaping (Error) -> Void) {

        self.onFinish = onFinish
        self.onCancel = onCancel
        self.onError = onError
    }

    func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
        onCancel()
    }

    func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
        var images: [UIImage] = []
        for page in 0..<scan.pageCount {
            images.append(scan.imageOfPage(at: page))
        }

        let scanModel = ScanModel(title: scan.title, images: images)
        onFinish(scanModel)
    }

    func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: any Error) {
        onError(error)
    }
}

struct ScanModel {
    let title: String
    let images: [UIImage]
}

VNDocumentCameraViewControllerUIViewControllerなので、SwiftUI で使うにはUIViewControllerRepresentableでラップしています。

func makeUIViewController(context: Context) -> VNDocumentCameraViewController {
        let viewController = VNDocumentCameraViewController()
        viewController.delegate = context.coordinator
        return viewController
    }

    func updateUIViewController(_ uiViewController: VNDocumentCameraViewController, context: Context) {}

    func makeCoordinator() -> ScannerCoordinator {

        return ScannerCoordinator(onFinish: onFinish, onCancel: onCancel, onError: onError)
    }

ScannerCoordinatorVNDocumentCameraViewControllerDelegateを準拠させ、
makeCoordinatorメソッドでScannerCoordinatorをインスタンス化して、
makeUIViewControllerメソッドでcontextからScannerCoordinatorを取り出して、delegateとして設定することで、Delegate を登録します。

final class ScannerCoordinator: NSObject, VNDocumentCameraViewControllerDelegate {

    private let onFinish: (ScanModel) -> Void
    private let onCancel: () -> Void
    private let onError: (Error) -> Void

    init(onFinish: @escaping (ScanModel) -> Void,
         onCancel: @escaping () -> Void,
         onError: @escaping (Error) -> Void) {

        self.onFinish = onFinish
        self.onCancel = onCancel
        self.onError = onError
    }

    func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
        onCancel()
    }

    func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
        var images: [UIImage] = []
        for page in 0..<scan.pageCount {
            images.append(scan.imageOfPage(at: page))
        }

        let scanModel = ScanModel(title: scan.title, images: images)
        onFinish(scanModel)
    }

    func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: any Error) {
        onError(error)
    }
}

VNDocumentCameraViewControllerDelegateでは、スキャン終了、キャンセル、エラーの 3 つのイベントを制御することができます。

スキャン終了

func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
        var images: [UIImage] = []
        for page in 0..<scan.pageCount {
            images.append(scan.imageOfPage(at: page))
        }

        let scanModel = ScanModel(title: scan.title, images: images)
        onFinish(scanModel)
    }

VNDocumentCameraScanを取得することができ、この中にスキャンした画像の情報が入っています。

上記例では、VNDocumentCameraScanから自前のScanModelというモデルクラスに変換して、onFinishで SwiftUI 側へ画像データを渡しています。

このメソッドが呼ばれるタイミングは、スキャン後に活性される"Save"ボタン押下時になります。(てっきりスキャン完了時によばれるものとおもっていた、、)

キャンセル

func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
        onCancel()
    }

そのままだけど、キャンセル時に呼ばれるメソッドです。
"Cancel"ボタン押下でアラートが表示されるので、そこで"Discard"を押下すると、このメソッドが呼ばれます。

エラー

func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: any Error) {
        onError(error)
    }

これもそのままで、スキャン中にエラーが発生したときに呼ばれます。

カスタマイズ性

ここまで見てきて、思うのはカスタマイズ性があるのだろうかというところが気になります。
delegate メソッドが呼ばれるタイミングがちょっと微妙(主にスキャン終了のタイミングが)なので、できればカスタマイズしたいですよね!
例えば、一回めにスキャンした書類をキャプチャときにスキャンを終了したいなどの時に、上記の delegate メソッドでは対応できないです。

ただ結論として、カスタマイズはできない!とフォーラムでガッツリ回答されていました。
残念、、

カスタマイズしたいなら、Vision フレームワークと CoreImage を使うことで自前実装するしかないとのことです

使い所

カスタマイズ性はないとはいえ、実装が簡単な割に相当リッチな機能を提供できると思います。
MVP として素早く書類スキャン機能を実装したい時に使う分には全然ありだと思いました。
MVP でウケが良かったら、自前実装で使いやすくするみたいにすると、開発側としてもビジネス側も嬉しくなりそうですね。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?