- 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>
144 lines
4.1 KiB
Swift
144 lines
4.1 KiB
Swift
import AVFoundation
|
|
import SwiftUI
|
|
|
|
class BarcodeScanner: NSObject, ObservableObject {
|
|
@Published var scannedCode: String?
|
|
@Published var isScanning = false
|
|
@Published var error: String?
|
|
|
|
private var captureSession: AVCaptureSession?
|
|
private let metadataOutput = AVCaptureMetadataOutput()
|
|
|
|
// Supported barcode types for food products
|
|
private let supportedTypes: [AVMetadataObject.ObjectType] = [
|
|
.ean8,
|
|
.ean13,
|
|
.upce,
|
|
.code128,
|
|
.code39,
|
|
.code93,
|
|
.itf14
|
|
]
|
|
|
|
override init() {
|
|
super.init()
|
|
}
|
|
|
|
// MARK: - Public Interface
|
|
func checkPermission() async -> Bool {
|
|
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
|
case .authorized:
|
|
return true
|
|
case .notDetermined:
|
|
return await AVCaptureDevice.requestAccess(for: .video)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func setupSession() {
|
|
guard captureSession == nil else { return }
|
|
|
|
let session = AVCaptureSession()
|
|
session.sessionPreset = .high
|
|
|
|
guard let videoDevice = AVCaptureDevice.default(for: .video),
|
|
let videoInput = try? AVCaptureDeviceInput(device: videoDevice) else {
|
|
error = "Camera not available"
|
|
return
|
|
}
|
|
|
|
if session.canAddInput(videoInput) {
|
|
session.addInput(videoInput)
|
|
}
|
|
|
|
if session.canAddOutput(metadataOutput) {
|
|
session.addOutput(metadataOutput)
|
|
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
|
metadataOutput.metadataObjectTypes = supportedTypes.filter {
|
|
metadataOutput.availableMetadataObjectTypes.contains($0)
|
|
}
|
|
}
|
|
|
|
captureSession = session
|
|
}
|
|
|
|
func startScanning() {
|
|
guard let session = captureSession else {
|
|
setupSession()
|
|
startScanning()
|
|
return
|
|
}
|
|
|
|
scannedCode = nil
|
|
error = nil
|
|
|
|
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
session.startRunning()
|
|
DispatchQueue.main.async {
|
|
self?.isScanning = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func stopScanning() {
|
|
captureSession?.stopRunning()
|
|
isScanning = false
|
|
}
|
|
|
|
func reset() {
|
|
scannedCode = nil
|
|
error = nil
|
|
}
|
|
|
|
var previewLayer: AVCaptureVideoPreviewLayer? {
|
|
guard let session = captureSession else { return nil }
|
|
let layer = AVCaptureVideoPreviewLayer(session: session)
|
|
layer.videoGravity = .resizeAspectFill
|
|
return layer
|
|
}
|
|
}
|
|
|
|
// MARK: - AVCaptureMetadataOutputObjectsDelegate
|
|
extension BarcodeScanner: AVCaptureMetadataOutputObjectsDelegate {
|
|
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
|
|
guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
|
|
let code = metadataObject.stringValue,
|
|
scannedCode == nil else { return }
|
|
|
|
// Haptic feedback
|
|
let generator = UIImpactFeedbackGenerator(style: .medium)
|
|
generator.impactOccurred()
|
|
|
|
scannedCode = code
|
|
stopScanning()
|
|
}
|
|
}
|
|
|
|
// MARK: - Camera Preview View
|
|
struct CameraPreviewView: UIViewRepresentable {
|
|
let scanner: BarcodeScanner
|
|
|
|
func makeUIView(context: Context) -> UIView {
|
|
let view = UIView(frame: .zero)
|
|
view.backgroundColor = .black
|
|
|
|
if let previewLayer = scanner.previewLayer {
|
|
previewLayer.frame = view.bounds
|
|
view.layer.addSublayer(previewLayer)
|
|
}
|
|
|
|
return view
|
|
}
|
|
|
|
func updateUIView(_ uiView: UIView, context: Context) {
|
|
DispatchQueue.main.async {
|
|
if let sublayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer {
|
|
sublayer.frame = uiView.bounds
|
|
} else if let previewLayer = scanner.previewLayer {
|
|
previewLayer.frame = uiView.bounds
|
|
uiView.layer.addSublayer(previewLayer)
|
|
}
|
|
}
|
|
}
|
|
}
|