DocumentScannerView.swift 103 lignes · 3606 octets
import SwiftUI
import VisionKit
import PhotosUI

/// SwiftUI wrapper around `VNDocumentCameraViewController`.
/// VisionKit performs capture + perspective correction on-device;
/// only the first scanned page is used (one business card per scan).
struct DocumentScannerView: UIViewControllerRepresentable {

    enum Outcome {
        case page(UIImage)
        case cancelled
        case failed(Error)
    }

    let completion: (Outcome) -> Void

    /// False in the simulator — use `PhotoPickerView` there.
    static var isSupported: Bool { VNDocumentCameraViewController.isSupported }

    func makeUIViewController(context: Context) -> VNDocumentCameraViewController {
        let controller = VNDocumentCameraViewController()
        controller.delegate = context.coordinator
        return controller
    }

    func updateUIViewController(_ uiViewController: VNDocumentCameraViewController, context: Context) {}

    func makeCoordinator() -> Coordinator { Coordinator(completion: completion) }

    final class Coordinator: NSObject, VNDocumentCameraViewControllerDelegate {
        private let completion: (Outcome) -> Void

        init(completion: @escaping (Outcome) -> Void) {
            self.completion = completion
        }

        func documentCameraViewController(
            _ controller: VNDocumentCameraViewController,
            didFinishWith scan: VNDocumentCameraScan
        ) {
            guard scan.pageCount > 0 else {
                completion(.cancelled)
                return
            }
            completion(.page(scan.imageOfPage(at: 0)))
        }

        func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
            completion(.cancelled)
        }

        func documentCameraViewController(
            _ controller: VNDocumentCameraViewController,
            didFailWithError error: Error
        ) {
            completion(.failed(error))
        }
    }
}

/// `PHPickerViewController` wrapper so the scan flow can be exercised in the
/// simulator with an imported image. Completion receives nil on cancel or when
/// the selected asset cannot be loaded as a UIImage.
struct PhotoPickerView: UIViewControllerRepresentable {

    let completion: (UIImage?) -> Void

    func makeUIViewController(context: Context) -> PHPickerViewController {
        var configuration = PHPickerConfiguration()
        configuration.filter = .images
        configuration.selectionLimit = 1
        let controller = PHPickerViewController(configuration: configuration)
        controller.delegate = context.coordinator
        return controller
    }

    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}

    func makeCoordinator() -> Coordinator { Coordinator(completion: completion) }

    final class Coordinator: NSObject, PHPickerViewControllerDelegate {
        private let completion: (UIImage?) -> Void

        init(completion: @escaping (UIImage?) -> Void) {
            self.completion = completion
        }

        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            guard let provider = results.first?.itemProvider,
                  provider.canLoadObject(ofClass: UIImage.self) else {
                completion(nil)
                return
            }
            let completion = completion
            provider.loadObject(ofClass: UIImage.self) { object, _ in
                DispatchQueue.main.async {
                    completion(object as? UIImage)
                }
            }
        }
    }
}