add ExerciseEditor, Picker skeletons, AutocompleteTextfield

This commit is contained in:
Felix Förtsch
2024-09-23 11:41:34 +02:00
parent 41b97964c4
commit 4a42fc6c33
20 changed files with 527 additions and 164 deletions

View File

@@ -0,0 +1,98 @@
//
// AutocompleteTextField.swift
// WorkoutsPlus
//
// Created by Felix Förtsch on 19.09.24.
//
import SwiftUI
struct AutocompleteTextField<Item: Nameable>: View {
var placeholder: String
@Binding var item: Item?
var items: [Item]
@State private var searchText = ""
var filteredItems: [Item] {
if searchText.isEmpty {
return []
} else {
return items.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}
}
@State private var isAutoCompleteShown: Bool = true
var body: some View {
Group {
HStack {
TextField(placeholder, text: $searchText)
// TODO: Fix "List line" not extending to the full width
if item == nil && !searchText.isEmpty {
Text("NEW")
.font(.caption)
.foregroundStyle(.green)
}
if item != nil {
Text("EDIT")
.font(.caption)
.foregroundStyle(.orange)
}
}
.onAppear() {
if item != nil {
self.searchText = item!.name
}
}
.onChange(of: searchText) {
if filteredItems.count == 1 && filteredItems.first?.name == searchText {
self.item = filteredItems.first
isAutoCompleteShown = false
} else {
self.item = nil
isAutoCompleteShown = true
}
}
if isAutoCompleteShown {
ForEach(filteredItems, id: \.self) { item in
HStack {
Text(item.name)
.foregroundStyle(.gray)
Spacer()
Image(systemName: "arrow.up.left")
.foregroundStyle(.blue)
}
// This .contentShape makes the whole row tappable
.contentShape(Rectangle())
.onTapGesture {
self.item = item
searchText = item.name
isAutoCompleteShown = false
}
}
}
}
}
}
private struct Item: Nameable {
var id = UUID()
var name: String
}
#Preview {
@Previewable @State var item: Item? = nil
VStack {
Text("Preview only: ")
Text(item?.name ?? "No item selected")
}
.background(.red)
List {
AutocompleteTextField<Item>(placeholder: "New Item", item: $item, items: [
Item(name: "Item 1"),
Item(name: "Item 2"),
Item(name: "Item 3")
])
}
}

View File

@@ -0,0 +1,22 @@
//
// Binding.swift
// WorkoutsPlus
//
// Created by Felix Förtsch on 21.09.24.
//
import SwiftUI
extension Binding {
init?(_ source: Binding<Value?>) {
guard let value = source.wrappedValue else {
return nil
}
self.init(
get: { value },
set: { newValue in
source.wrappedValue = newValue
}
)
}
}

View File

@@ -0,0 +1,31 @@
//
// DistancePicker.swift
// WorkoutsPlus
//
// Created by Felix Förtsch on 21.09.24.
//
import SwiftUI
struct DistancePicker: View {
@Binding var distance: String
var body: some View {
VStack {
TextField("Distance (m)", text: $distance)
.keyboardType(.numberPad)
if let distanceInMeters = Double(distance) {
let distanceInKilometers = distanceInMeters / 1000
Text("Distance: \(distanceInKilometers, specifier: "%.2f") km")
.font(.caption)
.foregroundColor(.gray)
}
}
}
}
#Preview {
@Previewable @State var distance = ""
DistancePicker(distance: $distance)
}

View File

@@ -0,0 +1,25 @@
//
// DurationPicker.swift
// WorkoutsPlus
//
// Created by Felix Förtsch on 21.09.24.
//
import SwiftUI
struct DurationPicker: View {
@Binding var duration: String
var body: some View {
// DatePicker("Duration", selection: $duration, displayedComponents: [.hourAndMinute, .date])
VStack {
TextField("Duration in seconds", text: $duration)
.keyboardType(.numberPad)
}
}
}
#Preview {
@Previewable @State var duration = "120"
DurationPicker(duration: $duration)
}

View File

@@ -27,6 +27,6 @@ struct ExerciseListItem: View {
#Preview {
List {
ExerciseListItem(Workout(name: "RR"), WorkoutItem(exercise: Exercise("Push-ups")))
ExerciseListItem(Workout(name: "RR"), WorkoutItem(exercise: Exercise("Push-ups", .reps)))
}
}

View File

@@ -1,16 +1,19 @@
//
// ItemPicker.swift
// WorkoutsPlus
// Advanced Version of Picker.pickerStyle(NavigationLinkPickerStyle()) that's searchable and has a ContentUnavailableView
//
// Created by Felix Förtsch on 10.09.24.
//
import SwiftUI
import SwiftData
struct ItemPicker<Item: Nameable>: View {
@Environment(\.dismiss) private var dismiss
@Binding var selectedItem: Item?
var items: [Item]
@State private var searchText = ""
var filteredItems: [Item] {
if searchText.isEmpty {
@@ -20,9 +23,6 @@ struct ItemPicker<Item: Nameable>: View {
}
}
var items: [Item]
@Binding var selectedItem: Item?
var body: some View {
List {
ForEach(filteredItems) { item in
@@ -51,10 +51,18 @@ struct ItemPicker<Item: Nameable>: View {
}
}
#Preview {
@Previewable @State var selectedWorkout: Workout? = nil
NavigationStack {
ItemPicker<Workout>(items: Workout.sampleData, selectedItem: $selectedWorkout)
}
.modelContainer(SampleData.shared.modelContainer)
private struct Item: Nameable {
var id = UUID()
var name: String
}
#Preview {
@Previewable @State var selectedItem: Item? = nil
NavigationStack {
ItemPicker<Item>(selectedItem: $selectedItem, items: [
Item(name: "Item 1"),
Item(name: "Item 2"),
Item(name: "Item 3")
])
}
}

View File

@@ -0,0 +1,23 @@
//
// RepsPicker.swift
// WorkoutsPlus
//
// Created by Felix Förtsch on 21.09.24.
//
import SwiftUI
// TODO: Think about implementing a custom keyboard like in FoodNoms
struct RepsPicker: View {
@Binding var reps: String
var body: some View {
TextField("Enter reps", text: $reps)
}
}
#Preview {
@Previewable @State var reps = ""
RepsPicker(reps: $reps)
}