I am trying to create an iOS app where when I scan a product, information about it comes up. When I scan a product, I want it to be stored using SwiftData and displayed in my History page. For some reason, whatever I have tried isn’t working.
Here’s the code I have so far:
This is the function I use to get information about a product. This is in my AppViewModel.swift.
import AVKit
import Foundation
import SwiftUI
import VisionKit
enum ScanType: String {
case barcode, text
}
enum DataScannerAccessStatusType {
case notDetermined
case cameraAccessNotGranted
case cameraNotAvailable
case scannerAvailable
case scannerNotAvailable
}
@MainActor
final class AppViewModel: ObservableObject {
@Environment(.modelContext) private var modelContext
@Published var dataScannerAccessStatus: DataScannerAccessStatusType = .notDetermined
@Published var recognizedItems: [RecognizedItem] = []
@Published var scanType: ScanType = .barcode
@Published var textContentType: DataScannerViewController.TextContentType?
@Published var recognizesMultipleItems = true
@Published var showNutritionInfo = false
@Published var productInfo: NutritionInfoView?
public func fetchProductInfo(barcode: String) {
let baseURL = "https://world.openfoodfacts.org/api/v0/product/"
guard let url = URL(string: "(baseURL)(barcode).json") else {
print("Invalid URL")
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Failed to retrieve data: (error.localizedDescription)")
return
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data else {
print("Failed to retrieve data. Status code: ((response as? HTTPURLResponse)?.statusCode ?? 0)")
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let status = json["status"] as? Int, status == 1,
let product = json["product"] as? [String: Any] {
let newProduct = Product(
withAdditives: product["with_additives"] as? String ?? "N/A",
name: product["product_name"] as? String ?? "N/A",
brand: product["brands"] as? String ?? "N/A",
quantity: product["quantity"] as? String ?? "N/A",
ingredients: product["ingredients_text"] as? String ?? "N/A",
nutritionScore: product["nutriscore_score"] as? Int ?? -1,
imageURL: product["image_url"] as? String ?? "N/A"
)
DispatchQueue.main.async {
self.productInfo = NutritionInfoView(product: newProduct)
self.showNutritionInfo = true
self.modelContext.insert(newProduct)
do {
try self.modelContext.save()
} catch {
print("Failed to save context: (error.localizedDescription)")
}
}
} else {
print("Product not found.")
}
} catch {
print("Failed to parse JSON: (error.localizedDescription)")
}
}
task.resume()
}
// Other code
}
This is my CameraView.swift:
import SwiftUI
import VisionKit
struct CameraView: View {
@EnvironmentObject var vm: AppViewModel
var body: some View {
switch vm.dataScannerAccessStatus {
case .scannerAvailable:
mainView
case .cameraNotAvailable:
Text("Your device doesn't have a camera")
case .scannerNotAvailable:
Text("Your device doesn't have support for scanning barcode with this app")
case .cameraAccessNotGranted:
Text("Please provide access to the camera in settings")
case .notDetermined:
Text("Requesting camera access")
}
}
private var mainView: some View {
ZStack {
DataScannerView(
recognizedItems: $vm.recognizedItems,
recognizedDataType: vm.recognizedDataType,
recognizesMultipleItems: vm.recognizesMultipleItems,
onScan: { result in
// Ensure scanType is barcode
if vm.scanType == .barcode {
// Iterate over recognized items
for item in result {
// Check if the item is a barcode
if case let .barcode(barcode) = item {
// Fetch product info with the barcode payload
vm.fetchProductInfo(barcode: barcode.payloadStringValue ?? "")
// Exit the loop after processing the first barcode (if needed)
break
}
}
}
}
)
.background { Color.gray.opacity(0.3) }
.ignoresSafeArea()
.id(vm.dataScannerViewId)
.sheet(isPresented: $vm.showNutritionInfo) {
// TODO: Make scrollable if not already, maybe through a ScrollablePane (idk what it's rally caleed look at barcode app I think it's there)
if let productInfo = vm.productInfo {
productInfo
.presentationDragIndicator(.visible)
}
}
VStack {
Spacer()
bottomContainerView
.background(.clear)
.frame(maxWidth: .infinity)
.frame(height: UIScreen.main.bounds.height * 0.35) // Height of scanning information
.clipped()
}
.edgesIgnoringSafeArea(.bottom)
.onChange(of: vm.scanType) { _ in vm.recognizedItems = [] }
.onChange(of: vm.textContentType) { _ in vm.recognizedItems = [] }
.onChange(of: vm.recognizesMultipleItems) { _ in vm.recognizedItems = [] }
}
}
private var bottomContainerView: some View {
VStack {
Picker("Scan Type", selection: $vm.scanType) {
Text("Barcode").tag(ScanType.barcode)
Text("Text").tag(ScanType.text)
}
.pickerStyle(.segmented)
.padding(.leading, 30)
.padding(.trailing, 30)
.padding(.horizontal)
}
}
}
This is my HistoryView.swift
import SwiftUI
import SwiftData
struct HistoryView: View {
@Environment(.modelContext) private var modelContext
@Query() private var products: [Product]
var body: some View {
NavigationSplitView {
Group {
if !products.isEmpty {
List {
ForEach(products) { product in
NavigationLink {
NutritionInfoView(product: product)
} label: {
Text(product.name)
}
}
.onDelete(perform: deleteItems)
}
} else {
ContentUnavailableView {
Label("You haven't scanned anything yet", systemImage: "barcode.viewfinder")
}
}
}
.navigationTitle("History")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
} detail: {
Text("Select a product")
.navigationTitle("Products")
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(products[index])
}
}
}
}
#Preview {
HistoryView()
}
All help is greatly appreciated!
1
Got the answer! Just need to pass in the model context to the fetchProductInfo() function because AppViewModel is an EnvironmentObject.