add ExerciseEditor, Picker skeletons, AutocompleteTextfield
This commit is contained in:
98
WorkoutsPlus/Components/AutocompleteTextField.swift
Normal file
98
WorkoutsPlus/Components/AutocompleteTextField.swift
Normal 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")
|
||||
])
|
||||
}
|
||||
}
|
||||
22
WorkoutsPlus/Components/Binding.swift
Normal file
22
WorkoutsPlus/Components/Binding.swift
Normal 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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
31
WorkoutsPlus/Components/DistancePicker.swift
Normal file
31
WorkoutsPlus/Components/DistancePicker.swift
Normal 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)
|
||||
}
|
||||
25
WorkoutsPlus/Components/DurationPicker.swift
Normal file
25
WorkoutsPlus/Components/DurationPicker.swift
Normal 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)
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
23
WorkoutsPlus/Components/RepsPicker.swift
Normal file
23
WorkoutsPlus/Components/RepsPicker.swift
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user