diff --git a/RIT Dining.xcodeproj/project.pbxproj b/RIT Dining.xcodeproj/project.pbxproj index ff834a0..cb8ddb6 100644 --- a/RIT Dining.xcodeproj/project.pbxproj +++ b/RIT Dining.xcodeproj/project.pbxproj @@ -265,7 +265,7 @@ CODE_SIGN_ENTITLEMENTS = "RIT Dining/RIT Dining.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; DEVELOPMENT_TEAM = 5GF7GKNTK4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -300,7 +300,7 @@ CODE_SIGN_ENTITLEMENTS = "RIT Dining/RIT Dining.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; + CURRENT_PROJECT_VERSION = 18; DEVELOPMENT_TEAM = 5GF7GKNTK4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; diff --git a/RIT Dining/Components/FDMealPlannerParsers.swift b/RIT Dining/Components/FDMealPlannerParsers.swift index 12d96ec..36ca91d 100644 --- a/RIT Dining/Components/FDMealPlannerParsers.swift +++ b/RIT Dining/Components/FDMealPlannerParsers.swift @@ -16,6 +16,11 @@ func parseFDMealPlannerMenu(menu: FDMealsParser) -> [FDMenuItem] { // will only be a single index to operate on. if let allMenuRecipes = menu.result[0].allMenuRecipes { for recipe in allMenuRecipes { + // Prevent duplicate items from being added, because for some reason the exact same item with the exact same information + // might be included in FD MealPlanner more than once. + if menuItems.contains(where: { $0.id == recipe.componentId }) { + continue + } // englishAlternateName holds the proper name of the item, but it's blank for some items for some reason. If that's the // case, then we should fall back on componentName, which is less user-friendly but works as a backup. let realName = if recipe.englishAlternateName != "" { @@ -23,7 +28,7 @@ func parseFDMealPlannerMenu(menu: FDMealsParser) -> [FDMenuItem] { } else { recipe.componentName } - let allergens = recipe.allergenName.components(separatedBy: ",") + let allergens = recipe.allergenName != "" ? recipe.allergenName.components(separatedBy: ",") : [] // Get the list of dietary markers (Vegan, Vegetarian, Pork, Beef), and drop "Vegetarian" if "Vegan" is also included since // that's kinda redundant. var dietaryMarkers = recipe.recipeProductDietaryName != "" ? recipe.recipeProductDietaryName.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } : [] @@ -31,6 +36,23 @@ func parseFDMealPlannerMenu(menu: FDMealsParser) -> [FDMenuItem] { dietaryMarkers.remove(at: dietaryMarkers.firstIndex(of: "Vegetarian")!) } let calories = Int(Double(recipe.calories)!.rounded()) + // Collect and organize all the nutritional entries. I ordered them based off how they were ordered in the nutritional + // facts panel on the side of the bag of goldfish that lives on my desk, so presumably they're ordered correctly. + let nutritionalEntries = [ + FDNutritionalEntry(type: "Total Fat", amount: Double(recipe.fat) ?? 0.0, unit: recipe.fatUOM), + FDNutritionalEntry(type: "Saturated Fat", amount: Double(recipe.saturatedFat) ?? 0.0, unit: recipe.saturatedFatUOM), + FDNutritionalEntry(type: "Trans Fat", amount: Double(recipe.transFattyAcid) ?? 0.0, unit: recipe.transFattyAcidUOM), + FDNutritionalEntry(type: "Cholesterol", amount: Double(recipe.cholesterol) ?? 0.0, unit: recipe.cholesterolUOM), + FDNutritionalEntry(type: "Sodium", amount: Double(recipe.sodium) ?? 0.0, unit: recipe.sodiumUOM), + FDNutritionalEntry(type: "Total Carbohydrates", amount: Double(recipe.carbohydrates) ?? 0.0, unit: recipe.carbohydratesUOM), + FDNutritionalEntry(type: "Dietary Fiber", amount: Double(recipe.dietaryFiber) ?? 0.0, unit: recipe.dietaryFiberUOM), + FDNutritionalEntry(type: "Total Sugars", amount: Double(recipe.totalSugars) ?? 0.0, unit: recipe.totalSugarsUOM), + FDNutritionalEntry(type: "Protein", amount: Double(recipe.protein) ?? 0.0, unit: recipe.proteinUOM), + FDNutritionalEntry(type: "Calcium", amount: Double(recipe.calcium) ?? 0.0, unit: recipe.calciumUOM), + FDNutritionalEntry(type: "Iron", amount: Double(recipe.iron) ?? 0.0, unit: recipe.ironUOM), + FDNutritionalEntry(type: "Vitamin A", amount: Double(recipe.vitaminA) ?? 0.0, unit: recipe.vitaminAUOM), + FDNutritionalEntry(type: "Vitamin C", amount: Double(recipe.vitaminC) ?? 0.0, unit: recipe.vitaminCUOM), + ] let newItem = FDMenuItem( id: recipe.componentId, @@ -39,6 +61,7 @@ func parseFDMealPlannerMenu(menu: FDMealsParser) -> [FDMenuItem] { category: recipe.category, allergens: allergens, calories: calories, + nutritionalEntries: nutritionalEntries, dietaryMarkers: dietaryMarkers, ingredients: recipe.ingredientStatement, price: recipe.sellingPrice, diff --git a/RIT Dining/Data/Static/TCtoFDMPMap.swift b/RIT Dining/Data/Static/TCtoFDMPMap.swift index 7b5d886..3e7dd03 100644 --- a/RIT Dining/Data/Static/TCtoFDMPMap.swift +++ b/RIT Dining/Data/Static/TCtoFDMPMap.swift @@ -20,6 +20,6 @@ let tCtoFDMPMap: [Int: (Int, Int)] = [ 441: (11, 11), // Loaded Latke 38: (12, 12), // Midnight Oil 26: (14, 4), // RITZ - 9041: (18, 17), // The College Grind + 35: (18, 17), // The College Grind 24: (15, 14), // The Commons ] diff --git a/RIT Dining/Data/Types/FDMealPlannerTypes.swift b/RIT Dining/Data/Types/FDMealPlannerTypes.swift index e83d9e7..d6bcd38 100644 --- a/RIT Dining/Data/Types/FDMealPlannerTypes.swift +++ b/RIT Dining/Data/Types/FDMealPlannerTypes.swift @@ -54,6 +54,32 @@ struct FDMealsParser: Decodable, Hashable { let category: String let allergenName: String let calories: String + let carbohydrates: String + let carbohydratesUOM: String + let dietaryFiber: String + let dietaryFiberUOM: String + let fat: String + let fatUOM: String + let protein: String + let proteinUOM: String + let saturatedFat: String + let saturatedFatUOM: String + let transFattyAcid: String + let transFattyAcidUOM: String + let calcium: String + let calciumUOM: String + let cholesterol: String + let cholesterolUOM: String + let iron: String + let ironUOM: String + let sodium: String + let sodiumUOM: String + let vitaminA: String + let vitaminAUOM: String + let vitaminC: String + let vitaminCUOM: String + let totalSugars: String + let totalSugarsUOM: String let recipeProductDietaryName: String let ingredientStatement: String let sellingPrice: Double @@ -74,6 +100,13 @@ struct FDMealsParser: Decodable, Hashable { let result: [Result] } +/// A single nutritional entry, including the amount and the unit. Used over a tuple for hashable purposes. +struct FDNutritionalEntry: Hashable { + let type: String + let amount: Double + let unit: String +} + /// A single menu item, stripped down and reorganized to a format that actually makes sense for me to use in the rest of the app. struct FDMenuItem: Hashable, Identifiable { let id: Int @@ -82,6 +115,7 @@ struct FDMenuItem: Hashable, Identifiable { let category: String let allergens: [String] let calories: Int + let nutritionalEntries: [FDNutritionalEntry] let dietaryMarkers: [String] let ingredients: String let price: Double diff --git a/RIT Dining/Views/MenuItemView.swift b/RIT Dining/Views/MenuItemView.swift index 58ac356..04aab64 100644 --- a/RIT Dining/Views/MenuItemView.swift +++ b/RIT Dining/Views/MenuItemView.swift @@ -59,15 +59,37 @@ struct MenuItemView: View { ) } } - Text("Allergens") - .font(.headline) - .padding(.top, 8) - Text(menuItem.allergens.joined(separator: ", ")) - .foregroundStyle(.secondary) - .textSelection(.enabled) - .padding(.bottom, 8) + .padding(.bottom, 12) + if !menuItem.allergens.isEmpty { + Text("Allergens") + .font(.title3) + .fontWeight(.semibold) + Text(menuItem.allergens.joined(separator: ", ")) + .foregroundStyle(.secondary) + .textSelection(.enabled) + .padding(.bottom, 12) + .onAppear { + print(menuItem.allergens) + } + } + VStack(alignment: .leading) { + Text("Nutrition Facts") + .font(.title3) + .fontWeight(.semibold) + ForEach(menuItem.nutritionalEntries, id: \.self) { entry in + HStack(alignment: .top) { + Text(entry.type) + Spacer() + Text("\(String(format: "%.1f", entry.amount))\(entry.unit)") + .foregroundStyle(.secondary) + } + Divider() + } + } + .padding(.bottom, 12) Text("Ingredients") - .font(.headline) + .font(.title3) + .fontWeight(.semibold) Text(menuItem.ingredients) .foregroundStyle(.secondary) .textSelection(.enabled) @@ -88,6 +110,7 @@ struct MenuItemView: View { category: "Baked Goods", allergens: ["Wheat", "Gluten", "Egg", "Milk", "Soy"], calories: 470, + nutritionalEntries: [FDNutritionalEntry(type: "Example", amount: 0.0, unit: "g")], dietaryMarkers: ["Vegetarian"], ingredients: "Some ingredients that you'd expect to find inside of a chocolate chip muffin", price: 2.79, diff --git a/RIT Dining/Views/MenuView.swift b/RIT Dining/Views/MenuView.swift index f9d95ce..0651cea 100644 --- a/RIT Dining/Views/MenuView.swift +++ b/RIT Dining/Views/MenuView.swift @@ -67,6 +67,9 @@ struct MenuView: View { let searchedLocations = searchText.isEmpty || item.name.localizedCaseInsensitiveContains(searchText) return searchedLocations } + newItems.sort { firstItem, secondItem in + return firstItem.name.localizedCaseInsensitiveCompare(secondItem.name) == .orderedAscending + } return newItems }