payfrit-food-ios/PayfritFood/Models/Product.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

174 lines
5.8 KiB
Swift

import Foundation
struct Product: Identifiable, Hashable {
static func == (lhs: Product, rhs: Product) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
let id: Int
let barcode: String
let name: String
let brand: String
let imageUrl: String?
let score: Int // 0-100 weighted index
let novaGroup: Int // 1-4
let servingSize: String
let calories: Double
let fat: Double
let saturatedFat: Double
let carbs: Double
let sugar: Double
let fiber: Double
let protein: Double
let sodium: Double
let ingredients: String
let dietaryTags: [String] // ["vegan", "gluten-free", etc.]
init(json: [String: Any]) {
// Handle nested structures from API
let rating = json["Rating"] as? [String: Any] ?? [:]
let nutrition = json["Nutrition"] as? [String: Any] ?? [:]
let dietary = json["Dietary"] as? [String: Any] ?? [:]
id = JSON.parseInt(json["id"] ?? json["ID"] ?? json["productId"])
barcode = JSON.parseString(json["barcode"] ?? json["Barcode"] ?? json["upc"])
name = JSON.parseString(json["name"] ?? json["Name"] ?? json["productName"])
brand = JSON.parseString(json["brand"] ?? json["Brand"] ?? json["brandName"])
imageUrl = JSON.parseOptionalString(json["imageUrl"] ?? json["ImageURL"] ?? json["image"])
// Score from Rating.OverallScore or top-level
score = JSON.parseInt(rating["OverallScore"] ?? json["score"] ?? json["Score"])
novaGroup = JSON.parseInt(json["novaGroup"] ?? json["NovaGroup"] ?? json["nova"])
// Nutrition from nested object or top-level
servingSize = JSON.parseString(nutrition["ServingSize"] ?? json["servingSize"] ?? json["ServingSize"])
calories = JSON.parseDouble(nutrition["Calories"] ?? json["calories"] ?? json["Calories"])
fat = JSON.parseDouble(nutrition["Fat"] ?? json["fat"] ?? json["Fat"])
saturatedFat = JSON.parseDouble(nutrition["SaturatedFat"] ?? json["saturatedFat"] ?? json["SaturatedFat"])
carbs = JSON.parseDouble(nutrition["Carbohydrates"] ?? json["carbs"] ?? json["Carbs"])
sugar = JSON.parseDouble(nutrition["Sugars"] ?? json["sugar"] ?? json["Sugar"])
fiber = JSON.parseDouble(nutrition["Fiber"] ?? json["fiber"] ?? json["Fiber"])
protein = JSON.parseDouble(nutrition["Protein"] ?? json["protein"] ?? json["Protein"])
sodium = JSON.parseDouble(nutrition["Sodium"] ?? json["sodium"] ?? json["Sodium"])
ingredients = JSON.parseString(json["ingredients"] ?? json["Ingredients"])
// Build dietary tags from Dietary object or use existing array
var tags: [String] = []
if JSON.parseBool(dietary["IsVegan"]) { tags.append("Vegan") }
if JSON.parseBool(dietary["IsVegetarian"]) { tags.append("Vegetarian") }
if JSON.parseBool(dietary["IsGlutenFree"]) { tags.append("Gluten-Free") }
if JSON.parseBool(dietary["IsDairyFree"]) { tags.append("Dairy-Free") }
if JSON.parseBool(dietary["IsNutFree"]) { tags.append("Nut-Free") }
if JSON.parseBool(dietary["IsOrganic"]) { tags.append("Organic") }
if tags.isEmpty {
dietaryTags = JSON.parseStringArray(json["dietaryTags"] ?? json["DietaryTags"] ?? json["tags"])
} else {
dietaryTags = tags
}
}
init(
id: Int,
barcode: String,
name: String,
brand: String,
imageUrl: String? = nil,
score: Int,
novaGroup: Int,
servingSize: String,
calories: Double,
fat: Double,
saturatedFat: Double,
carbs: Double,
sugar: Double,
fiber: Double,
protein: Double,
sodium: Double,
ingredients: String,
dietaryTags: [String]
) {
self.id = id
self.barcode = barcode
self.name = name
self.brand = brand
self.imageUrl = imageUrl
self.score = score
self.novaGroup = novaGroup
self.servingSize = servingSize
self.calories = calories
self.fat = fat
self.saturatedFat = saturatedFat
self.carbs = carbs
self.sugar = sugar
self.fiber = fiber
self.protein = protein
self.sodium = sodium
self.ingredients = ingredients
self.dietaryTags = dietaryTags
}
// MARK: - Computed Properties
var imageURL: URL? {
guard let imageUrl = imageUrl else { return nil }
return URL(string: imageUrl)
}
var scoreColor: ScoreColor {
switch score {
case 80...100: return .excellent
case 50..<80: return .good
case 30..<50: return .fair
default: return .poor
}
}
var novaColor: NovaColor {
switch novaGroup {
case 1: return .nova1
case 2: return .nova2
case 3: return .nova3
default: return .nova4
}
}
enum ScoreColor {
case excellent, good, fair, poor
var color: String {
switch self {
case .excellent: return "scoreGreen"
case .good: return "scoreYellow"
case .fair: return "scoreOrange"
case .poor: return "scoreRed"
}
}
}
enum NovaColor {
case nova1, nova2, nova3, nova4
var color: String {
switch self {
case .nova1: return "novaGreen"
case .nova2: return "novaYellow"
case .nova3: return "novaOrange"
case .nova4: return "novaRed"
}
}
var label: String {
switch self {
case .nova1: return "Unprocessed"
case .nova2: return "Processed ingredients"
case .nova3: return "Processed"
case .nova4: return "Ultra-processed"
}
}
}
}