feat: add iOS support and harden receipt scanning

This commit is contained in:
Timo
2026-08-20 23:48:26 +02:00
parent f5e06c32b0
commit cf1b799e1b
107 changed files with 11755 additions and 122 deletions

View File

@@ -0,0 +1,65 @@
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)
}
}
}
}