// // GenericItemManager.swift // WorkoutsPlus // // Created by Felix Förtsch on 15.11.24. // import SwiftUI import SwiftData // MARK: - Generic Item Manager struct GenericItemManager: View { @Environment(\.modelContext) private var modelContext @Query private var items: [T] @State private var showingNewItemSheet = false @State private var newItem: T? let title: String let createNew: () -> T init( title: String, sortBy: SortDescriptor, createNew: @escaping () -> T ) { self.title = title self.createNew = createNew _items = Query(sort: [sortBy]) } var body: some View { List { ForEach(items) { item in NavigationLink { ItemEditor(item: item) } label: { Text(String(describing: item)) } } .onDelete(perform: deleteItems) } .navigationTitle(title) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { let newItem = createNew() modelContext.insert(newItem) showingNewItemSheet = true self.newItem = newItem } label: { Image(systemName: "plus") } } } .sheet(isPresented: $showingNewItemSheet) { if let newItem { NavigationStack { ItemEditor(item: newItem) } } } } private func deleteItems(offsets: IndexSet) { for index in offsets { modelContext.delete(items[index]) } } } // MARK: - Item Editor struct ItemEditor: View { @Environment(\.dismiss) private var dismiss @Bindable var item: T var body: some View { Form { ForEach(T.editableProperties) { property in if property.type == String.self { TextField(property.displayName, text: Binding( get: { property.get(item) }, set: { property.set(item, $0) } )) } } } .navigationTitle("New \(String(describing: T.self))") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } } } } } // MARK: - Generic Assignment View struct GenericAssignmentView: View { @Environment(\.modelContext) private var modelContext @Query private var availableItems: [Item] @Bindable var owner: Owner let title: String let getAssignedItems: (Owner) -> [Item] let assign: (Owner, Item) -> Void let unassign: (Owner, Item) -> Void let createNew: () -> Item let sortBy: SortDescriptor init( title: String, owner: Owner, sortBy: SortDescriptor, getAssignedItems: @escaping (Owner) -> [Item], assign: @escaping (Owner, Item) -> Void, unassign: @escaping (Owner, Item) -> Void, createNew: @escaping () -> Item ) { self.title = title self.owner = owner self.sortBy = sortBy self.getAssignedItems = getAssignedItems self.assign = assign self.unassign = unassign self.createNew = createNew _availableItems = Query(sort: [sortBy]) } var body: some View { List { Section { ForEach(availableItems) { item in let isAssigned = getAssignedItems(owner).contains(where: { $0.id == item.id }) Button { if isAssigned { unassign(owner, item) } else { assign(owner, item) } } label: { HStack { Text(String(describing: item)) Spacer() if isAssigned { Image(systemName: "checkmark") } } } } } Section { NavigationLink { GenericItemManager( title: title, sortBy: sortBy, createNew: createNew ) } label: { Text("Manage \(title)") } } } .navigationTitle(title) } } // MARK: - Assignment Button struct AssignmentButton: View { @Environment(\.modelContext) private var modelContext @Query private var availableItems: [Item] let title: String let owner: Owner let getAssignedItems: (Owner) -> [Item] let sortBy: SortDescriptor let assign: (Owner, Item) -> Void let unassign: (Owner, Item) -> Void let createNew: () -> Item init( title: String, owner: Owner, sortBy: SortDescriptor, getAssignedItems: @escaping (Owner) -> [Item], assign: @escaping (Owner, Item) -> Void, unassign: @escaping (Owner, Item) -> Void, createNew: @escaping () -> Item ) { self.title = title self.owner = owner self.sortBy = sortBy self.getAssignedItems = getAssignedItems self.assign = assign self.unassign = unassign self.createNew = createNew _availableItems = Query(sort: [sortBy]) } var body: some View { switch availableItems.count { case 0: NavigationLink { GenericItemManager( title: title, sortBy: sortBy, createNew: createNew ) } label: { Text("Manage \(title)") } case 1...3: List { Section(header: Text("\(title)")) { ForEach(availableItems) { item in let isAssigned = getAssignedItems(owner).contains(where: { $0.id == item.id }) Button { if isAssigned { unassign(owner, item) } else { assign(owner, item) } } label: { HStack { Text(String(describing: item)) Spacer() if isAssigned { Image(systemName: "checkmark") } } } .foregroundStyle(.primary) } } Section { NavigationLink { GenericItemManager( title: title, sortBy: sortBy, createNew: createNew ) } label: { Text("Manage \(title)") } } } default: NavigationLink { GenericAssignmentView( title: title, owner: owner, sortBy: sortBy, getAssignedItems: getAssignedItems, assign: assign, unassign: unassign, createNew: createNew ) } label: { HStack { Text(title) Spacer() Text("\(getAssignedItems(owner).count) of \(availableItems.count)") .foregroundStyle(.gray) } } } } } // Protocol for editable properties protocol EditableModel { static var editableProperties: [EditableProperty] { get } } struct EditableProperty: Identifiable { let id = UUID() let displayName: String let type: Any.Type let get: (Any) -> String let set: (Any, String) -> Void }