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 UIImages, 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.