If you’re like me, you love the document scanner Apple’s notes app has: it always creates crisp scans and fully replaces a dedicated scanner for me. Be it receipts or letters, I have scanned it. But it’s not just reserved for Apple Notes, you can also integrate it with your app (elegantly called VNDocumentCameraViewController).
This is a small guide to get you started with a pluggable component. If you’re familiar with SwiftUI’s Coordinator pattern, this blog post do exactly this.
Creating the DocumentScanner view
So first, you should create a view based on UIViewControllerRepresentable
, I’m calling mine DocumentScanner
. You can also already add the Scanner’s coordinator which takes in a VNDocumentCameraViewControllerDelegate
struct DocumentScanner: UIViewControllerRepresentable {
class Coordinator: NSObject, VNDocumentCameraViewControllerDelegate {
}
func makeUIViewController(context: Context) -> VNDocumentCameraViewController {
}
func updateUIViewController(_ uiViewController: VNDocumentCameraViewController, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
typealias UIViewControllerType = VNDocumentCameraViewController
}
Our desired content is the VNDocumentCameraScan, which contains a list of UIImage
s, so we’re giving our DocumentScanner a binding that returns an array of UIImage and we can already construct the controller:
@Binding var images: [UIImage]
func makeUIViewController(context: Context) -> VNDocumentCameraViewController {
let camera = VNDocumentCameraViewController()
camera.delegate = context.coordinator
return camera
}
Note that we’re passing self
to the coordinator so we can access its parent from within the delegate. Let’s take a look at what we’re doing there. I have also added a $showScanner binding, as I’m embedding it within another view, but you can also call controller.dismiss(animating: true)
instead of showScanner = false
.
class Coordinator: NSObject, VNDocumentCameraViewControllerDelegate {
var parent: DocumentScanner
init(_ parent: DocumentScanner) {
self.parent = parent
}
func documentCameraViewController(
_ controller: VNDocumentCameraViewController,
didFinishWith scan: VNDocumentCameraScan
) {
parent.showScanner = false
// get all document images
if (scan.pageCount > 0) {
parent.images = [] // reset for a new scan
for pageIndex in 0...scan.pageCount-1 {
parent.images.append(scan.imageOfPage(at: pageIndex))
}
}
}
func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
parent.showScanner = false
parent.images = [] // reset for a new scan
}
func documentCameraViewController(
_ controller: VNDocumentCameraViewController,
didFailWithError error: Error
) {
parent.showScanner = false
}
}
And that’s already it. Now, you can embed it or show it as a modal (I’m doing first):
// prompt scan
NavigationLink(destination: DocumentScanner(images: $images, showScanner: $showScanner), isActive: $showScanner) {
Text("Scan documents")
}
// show scanned images
ForEach($images) { $i in
Image(uiImage: i)
.resizable()
.scaledToFit()
.frame(maxWidth: 100)
.cornerRadius(12)
}
You should also note that your app needs to have permission to access the camera, but best practices for this have already often been discussed elsewhere.