This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
133 lines
3.7 KiB
Swift
133 lines
3.7 KiB
Swift
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]
|
|
}
|
|
}
|
|
}
|