add GenericItemManager, fix small inconsistencies
This commit is contained in:
@@ -14,8 +14,7 @@ struct AddItemButton: View {
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
Image(systemName: "plus")
|
||||
Text(label)
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
|
||||
289
WorkoutsPlus/Components/GenericItemManager.swift
Normal file
289
WorkoutsPlus/Components/GenericItemManager.swift
Normal file
@@ -0,0 +1,289 @@
|
||||
//
|
||||
// GenericItemManager.swift
|
||||
// WorkoutsPlus
|
||||
//
|
||||
// Created by Felix Förtsch on 15.11.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
// MARK: - Generic Item Manager
|
||||
struct GenericItemManager<T: PersistentModel & Identifiable & CustomStringConvertible & EditableModel>: 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<T>,
|
||||
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<T: PersistentModel & EditableModel>: 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<Owner: PersistentModel, Item: PersistentModel & Identifiable & CustomStringConvertible & EditableModel>: 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<Item>
|
||||
|
||||
init(
|
||||
title: String,
|
||||
owner: Owner,
|
||||
sortBy: SortDescriptor<Item>,
|
||||
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<Item>(
|
||||
title: title,
|
||||
sortBy: sortBy,
|
||||
createNew: createNew
|
||||
)
|
||||
} label: {
|
||||
Text("Manage \(title)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(title)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Assignment Button
|
||||
struct AssignmentButton<Owner: PersistentModel, Item: PersistentModel & Identifiable & CustomStringConvertible & EditableModel>: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query private var availableItems: [Item]
|
||||
|
||||
let title: String
|
||||
let owner: Owner
|
||||
let getAssignedItems: (Owner) -> [Item]
|
||||
let sortBy: SortDescriptor<Item>
|
||||
let assign: (Owner, Item) -> Void
|
||||
let unassign: (Owner, Item) -> Void
|
||||
let createNew: () -> Item
|
||||
|
||||
init(
|
||||
title: String,
|
||||
owner: Owner,
|
||||
sortBy: SortDescriptor<Item>,
|
||||
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<Item>(
|
||||
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<Item>(
|
||||
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
|
||||
}
|
||||
@@ -9,16 +9,40 @@ import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class Equipment: Nameable {
|
||||
static var systemImage = "dumbbell.fill"
|
||||
|
||||
final class Equipment {
|
||||
var id = UUID()
|
||||
var creationDate: Date = Date.now
|
||||
|
||||
@Attribute(.unique) var name: String
|
||||
var exercises: [Exercise] = []
|
||||
@Attribute(.unique)
|
||||
var name: String
|
||||
|
||||
init(name: String) {
|
||||
init(name: String = "") {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
// This protocol is required to display the name if the equipment in the List.
|
||||
extension Equipment: CustomStringConvertible {
|
||||
var description: String {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
extension Equipment: EditableModel {
|
||||
static var editableProperties: [EditableProperty] {
|
||||
[
|
||||
EditableProperty(
|
||||
displayName: "Name",
|
||||
type: String.self,
|
||||
get: { item in
|
||||
guard let equipment = item as? Equipment else { return "" }
|
||||
return equipment.name
|
||||
},
|
||||
set: { item, newValue in
|
||||
guard let equipment = item as? Equipment else { return }
|
||||
equipment.name = newValue
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,24 @@ struct ExerciseEditor: View {
|
||||
}
|
||||
.pickerStyle(NavigationLinkPickerStyle())
|
||||
}
|
||||
|
||||
|
||||
AssignmentButton<Exercise, Equipment>(
|
||||
title: "Equipment",
|
||||
owner: exercise,
|
||||
sortBy: SortDescriptor(\Equipment.name),
|
||||
getAssignedItems: { exercise in
|
||||
exercise.equipment
|
||||
},
|
||||
assign: { exercise, equipment in
|
||||
exercise.equipment.append(equipment)
|
||||
},
|
||||
unassign: { exercise, equipment in
|
||||
exercise.equipment.removeAll(where: { $0.id == equipment.id })
|
||||
},
|
||||
createNew: { Equipment(name: "") }
|
||||
)
|
||||
|
||||
Section(footer: Text("Feature coming soon.")) {
|
||||
Toggle(isOn: $isPartOfProgression) {
|
||||
Text("Exercise is Part of a Progression")
|
||||
|
||||
Reference in New Issue
Block a user