import SwiftUI import VisionKit /// Wraps `VNDocumentCameraViewController` (the system document-scanning /// camera UI) for SwiftUI. Only the first scanned page is used for v1 — good /// enough for a single-receipt photo, and the backend already accepts a /// single image per `/api/scan` call. /// /// IMPORTANT: `VNDocumentCameraViewController.isSupported` is `false` on the /// Simulator and on devices without a usable camera. Callers must check that /// before presenting this view and fall back to the photo picker instead — /// this type does not check it itself. struct DocumentCameraView: UIViewControllerRepresentable { /// Called exactly once with the captured page, or `nil` if the user /// cancelled or the scan failed. Never left uncalled, so the caller never /// hangs waiting on a result. var onComplete: (UIImage?) -> Void func makeUIViewController(context: Context) -> VNDocumentCameraViewController { let controller = VNDocumentCameraViewController() controller.delegate = context.coordinator return controller } func updateUIViewController(_ uiViewController: VNDocumentCameraViewController, context: Context) { // No dynamic updates needed. } func makeCoordinator() -> Coordinator { Coordinator(onComplete: onComplete) } final class Coordinator: NSObject, VNDocumentCameraViewControllerDelegate { private let onComplete: (UIImage?) -> Void init(onComplete: @escaping (UIImage?) -> Void) { self.onComplete = onComplete } func documentCameraViewController( _ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan ) { let image = scan.pageCount > 0 ? scan.imageOfPage(at: 0) : nil controller.dismiss(animated: true) { self.onComplete(image) } } func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) { controller.dismiss(animated: true) { self.onComplete(nil) } } func documentCameraViewController( _ controller: VNDocumentCameraViewController, didFailWithError error: Error ) { controller.dismiss(animated: true) { self.onComplete(nil) } } } }