// // GroceryListTabView.swift // Nextcloud Cookbook iOS Client // // Created by Vincent Meilinger on 23.01.24. // import Foundation import SwiftUI import SwiftData @Model class GroceryItem { var name: String var isChecked: Bool init(name: String, isChecked: Bool) { self.name = name self.isChecked = isChecked } } @Model class RecipeGroceries: Identifiable { var id: String var name: String @Relationship(deleteRule: .cascade) var items: [GroceryItem] var multiplier: Double init(id: String, name: String, items: [GroceryItem], multiplier: Double) { self.id = id self.name = name self.items = items self.multiplier = multiplier } init(id: String, name: String) { self.id = id self.name = name self.items = [] self.multiplier = 1 } } struct GroceryListTabView: View { @Environment(\.modelContext) var modelContext @Query var groceryList: [RecipeGroceries] = [] @State var newGroceries: String = "" @FocusState private var isFocused: Bool var body: some View { NavigationStack { List { HStack(alignment: .top) { TextEditor(text: $newGroceries) .padding(4) .overlay(RoundedRectangle(cornerRadius: 8) .stroke(Color.secondary).opacity(0.5)) .focused($isFocused) Button { if !newGroceries.isEmpty { let items = newGroceries .split(separator: "\n") .compactMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } Task { await addGroceryItems(items, toCategory: "Other", named: String(localized: "Other")) } } newGroceries = "" } label: { Text("Add") } .disabled(newGroceries.isEmpty) .buttonStyle(.borderedProminent) } ForEach(groceryList, id: \.name) { category in Section { ForEach(category.items, id: \.self) { item in GroceryListItemView(item: item) } } header: { HStack { Text(category.name) .foregroundStyle(Color.nextcloudBlue) Spacer() Button { modelContext.delete(category) } label: { Image(systemName: "trash") .foregroundStyle(Color.nextcloudBlue) } } } } if groceryList.isEmpty { Text("You're all set for cooking 🍓") .font(.headline) Text("Add groceries to this list by either using the button next to an ingredient list in a recipe, or by swiping right on individual ingredients of a recipe.") .foregroundStyle(.secondary) Text("To add grocieries manually, type them in the box below and press the button. To add multiple items at once, separate them by a new line.") .foregroundStyle(.secondary) Text("Your grocery list is stored locally and therefore not synchronized across your devices.") .foregroundStyle(.secondary) } } .listStyle(.plain) .navigationTitle("Grocery List") .toolbar { Button { do { try modelContext.delete(model: RecipeGroceries.self) } catch { print("Failed to delete all GroceryCategory models.") } } label: { Text("Delete") .foregroundStyle(Color.nextcloudBlue) } } } } private func addGroceryItems(_ itemNames: [String], toCategory categoryId: String, named name: String) async { do { // Find or create the target category let categoryPredicate = #Predicate { $0.id == categoryId } let fetchDescriptor = FetchDescriptor(predicate: categoryPredicate) var targetCategory: RecipeGroceries? if let existingCategory = try modelContext.fetch(fetchDescriptor).first { targetCategory = existingCategory } else { // Create the category if it doesn't exist let newCategory = RecipeGroceries(id: categoryId, name: name) modelContext.insert(newCategory) targetCategory = newCategory } guard let category = targetCategory else { return } // Add new GroceryItems to the category for itemName in itemNames { let newItem = GroceryItem(name: itemName, isChecked: false) category.items.append(newItem) } try modelContext.save() } catch { print("Error adding grocery items: \(error.localizedDescription)") } } private func deleteGroceryItems(at offsets: IndexSet, in category: RecipeGroceries) { for index in offsets { let itemToDelete = category.items[index] modelContext.delete(itemToDelete) } } } fileprivate struct GroceryListItemView: View { @Environment(\.modelContext) var modelContext @Bindable var item: GroceryItem var body: some View { HStack(alignment: .top) { if item.isChecked { Image(systemName: "checkmark.circle") } else { Image(systemName: "circle") } Text("\(item.name)") .multilineTextAlignment(.leading) .lineLimit(5) } .padding(5) .foregroundStyle(item.isChecked ? Color.secondary : Color.primary) .onTapGesture(perform: { item.isChecked.toggle() }) .animation(.easeInOut, value: item.isChecked) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(action: { modelContext.delete(item) }) { Label("Delete", systemImage: "trash") } .tint(.red) } } }