iOSで書類をスキャンしたいと思った時に、なんか小難しいことしないとなのかなと漠然と思いつつ調べていたら、想像以上に簡単にできそうなAPIが見つかりました。
VNDocumentCameraViewController
が VisionKit フレームワークに iOS13 から登場していて技術調査がてら試しに使ってみたので、使用方法など記事にしてみます。
使い方はめちゃくちゃ簡単で、VNDocumentCameraViewController
をインスタンス化して、名前の通りUIViewController
なのでpresent
なりpush
なりで表示するだけで、以下のような UI を提供してくれます。
注意点として、カメラを使用するので、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]
}
VNDocumentCameraViewController
はUIViewController
なので、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)
}
ScannerCoordinator
にVNDocumentCameraViewControllerDelegate
を準拠させ、
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 でウケが良かったら、自前実装で使いやすくするみたいにすると、開発側としてもビジネス側も嬉しくなりそうですね。