import Combine import Foundation import SwiftUI enum ScanRoute: Hashable { case result case manual } @MainActor final class ScanFlowModel: ObservableObject { @Published var path: [ScanRoute] = [] @Published var currentSession: RecognitionSession? @Published var isRecognizing = false @Published var transientMessage: String? let cameraService: CameraService let photoLibraryService: PhotoLibraryService let networkMonitor: NetworkMonitor private let pipeline: CardRecognitionPipeline init() { let cameraService = CameraService() let photoLibraryService = PhotoLibraryService() let networkMonitor = NetworkMonitor() self.cameraService = cameraService self.photoLibraryService = photoLibraryService self.networkMonitor = networkMonitor self.pipeline = CardRecognitionPipeline( networkStatusProvider: networkMonitor, cloudOCRClient: StubCloudOCRClient(), fallbackOCR: VisionCardOCRService(), enhancer: NoOpCardFieldEnhancer() ) } func startObservers() { networkMonitor.startMonitoring() } func stopObservers() { networkMonitor.stopMonitoring() cameraService.stopSession() } func prepareVisibleServices() async { await cameraService.prepareIfAuthorized() await photoLibraryService.refreshRecentsIfPossible() } func enableCamera() async { await cameraService.requestAccessAndStart() } func capturePhoto() async { do { isRecognizing = true let image = try await cameraService.capturePhoto() try await recognize(image: image) } catch { transientMessage = error.localizedDescription } isRecognizing = false } func importRecentPhoto(_ item: RecentPhotoItem) async { isRecognizing = true defer { isRecognizing = false } guard let image = await photoLibraryService.loadImage(for: item) else { transientMessage = "The selected photo could not be loaded." return } do { try await recognize(image: image) } catch { currentSession = RecognitionSession( draft: CardRecognitionDraft.manualPrefill(rawText: currentSession?.draft.rawText ?? ""), thumbnailJPEGData: currentSession?.thumbnailJPEGData ) path = [.manual] transientMessage = error.localizedDescription } } func startManualEntry() { if currentSession == nil { currentSession = RecognitionSession(draft: .manualPrefill()) } else { currentSession?.draft.source = .manual currentSession?.draft.confidence = .low } path = [.manual] } func openManualEntryFromResult() { guard currentSession != nil else { startManualEntry() return } currentSession?.draft.source = .manual if path.last != .manual { path.append(.manual) } } func updateDraft(_ draft: CardRecognitionDraft) { currentSession?.draft = draft } func returnToScan(message: String? = nil) { currentSession = nil path = [] transientMessage = message } private func recognize(image: UIImage) async throws { let session = try await pipeline.recognizeCard(in: image) currentSession = session if session.draft.hasDetectedContent { path = [.result] } else { currentSession?.draft.source = .manual currentSession?.draft.confidence = .low path = [.manual] } } }