payfrit-food-ios/PayfritFood/Services/BarcodeScanner.swift
John Pinkyfloyd 71e7ec34f6 Initial commit: PayfritFood iOS app
- 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>
2026-03-16 16:58:21 -07:00

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)
}
}
}
}