- SwiftUI + async/await architecture - Barcode scanning with AVFoundation - Product display with score ring, NOVA badge, nutrition - Alternatives with sort/filter - Auth (login/register) - Favorites & history - Account management - Dark theme - Connected to food.payfrit.com API (Open Food Facts proxy) Co-Authored-By: Claude <noreply@anthropic.com>
164 lines
6.1 KiB
Swift
164 lines
6.1 KiB
Swift
import SwiftUI
|
|
|
|
struct ScanScreen: View {
|
|
@EnvironmentObject var appState: AppState
|
|
@StateObject private var scanner = BarcodeScanner()
|
|
@State private var showManualEntry = false
|
|
@State private var isLoading = false
|
|
@State private var showProduct = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ZStack {
|
|
Color.black.ignoresSafeArea()
|
|
|
|
if scanner.isScanning {
|
|
CameraPreviewView(scanner: scanner)
|
|
.ignoresSafeArea()
|
|
|
|
// Scan overlay
|
|
VStack {
|
|
Spacer()
|
|
|
|
// Scan region indicator
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.strokeBorder(Color.green, lineWidth: 3)
|
|
.frame(width: 280, height: 180)
|
|
.overlay {
|
|
Text("Position barcode here")
|
|
.font(.caption)
|
|
.foregroundColor(.white.opacity(0.7))
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Manual entry button
|
|
Button {
|
|
showManualEntry = true
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "keyboard")
|
|
Text("Enter Manually")
|
|
}
|
|
.font(.headline)
|
|
.foregroundColor(.white)
|
|
.padding()
|
|
.background(Color.white.opacity(0.2))
|
|
.cornerRadius(12)
|
|
}
|
|
.padding(.bottom, 40)
|
|
}
|
|
} else if isLoading {
|
|
VStack(spacing: 20) {
|
|
ProgressView()
|
|
.scaleEffect(1.5)
|
|
.tint(.white)
|
|
Text("Looking up product...")
|
|
.foregroundColor(.white)
|
|
}
|
|
} else {
|
|
// Start scan prompt
|
|
VStack(spacing: 30) {
|
|
Image(systemName: "barcode.viewfinder")
|
|
.font(.system(size: 80))
|
|
.foregroundColor(.green)
|
|
|
|
Text("Scan a Barcode")
|
|
.font(.title)
|
|
.foregroundColor(.white)
|
|
|
|
Text("Point your camera at a food product barcode to see its health rating and find healthier alternatives.")
|
|
.font(.body)
|
|
.foregroundColor(.gray)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 40)
|
|
|
|
Button {
|
|
Task {
|
|
let hasPermission = await scanner.checkPermission()
|
|
if hasPermission {
|
|
scanner.startScanning()
|
|
}
|
|
}
|
|
} label: {
|
|
Text("Start Scanning")
|
|
.font(.headline)
|
|
.foregroundColor(.black)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(Color.green)
|
|
.cornerRadius(12)
|
|
}
|
|
.padding(.horizontal, 40)
|
|
|
|
Button {
|
|
showManualEntry = true
|
|
} label: {
|
|
Text("Enter Barcode Manually")
|
|
.font(.subheadline)
|
|
.foregroundColor(.green)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Scan")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbarColorScheme(.dark, for: .navigationBar)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
if scanner.isScanning {
|
|
Button("Cancel") {
|
|
scanner.stopScanning()
|
|
}
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showManualEntry) {
|
|
ManualEntrySheet { barcode in
|
|
lookupProduct(barcode: barcode)
|
|
}
|
|
}
|
|
.navigationDestination(isPresented: $showProduct) {
|
|
if let product = appState.currentProduct {
|
|
ProductScreen(product: product)
|
|
.environmentObject(appState)
|
|
}
|
|
}
|
|
.onChange(of: scanner.scannedCode) { newCode in
|
|
if let code = newCode {
|
|
lookupProduct(barcode: code)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func lookupProduct(barcode: String) {
|
|
isLoading = true
|
|
scanner.stopScanning()
|
|
|
|
Task {
|
|
do {
|
|
let product = try await APIService.shared.lookupProduct(barcode: barcode)
|
|
await MainActor.run {
|
|
appState.setCurrentProduct(product)
|
|
isLoading = false
|
|
showProduct = true
|
|
|
|
// Add to history if authenticated
|
|
if appState.isAuthenticated {
|
|
Task {
|
|
try? await APIService.shared.addToHistory(productId: product.id)
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
isLoading = false
|
|
appState.showError(error.localizedDescription)
|
|
scanner.reset()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|