add GenericItemManager, fix small inconsistencies

This commit is contained in:
Felix Förtsch
2024-11-15 16:49:49 +01:00
parent 02e2937094
commit 1668b29803
4 changed files with 338 additions and 8 deletions

View File

@@ -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)
}

View 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
}

View File

@@ -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
}
)
]
}
}

View File

@@ -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")