Add CoreData-based workout tracking app with iOS and watchOS targets
- Migrate from SwiftData to CoreData with CloudKit sync - Add core models: Split, Exercise, Workout, WorkoutLog - Implement tab-based UI: Workout Logs, Splits, Settings - Add SF Symbols picker for split icons - Add exercise picker filtered by split with exclusion of added exercises - Integrate IndieAbout for settings/about section - Add Yams for YAML exercise definition parsing - Include starter exercise libraries (bodyweight, Planet Fitness) - Add Date extensions for formatting (formattedTime, isSameDay) - Format workout date ranges to show time-only for same-day end dates - Add build number update script - Add app icons
This commit is contained in:
228
Workouts/Views/Common/SFSymbolPicker.swift
Normal file
228
Workouts/Views/Common/SFSymbolPicker.swift
Normal file
@@ -0,0 +1,228 @@
|
||||
//
|
||||
// SFSymbolPicker.swift
|
||||
// Workouts
|
||||
//
|
||||
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SFSymbolPicker: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Binding var selection: String
|
||||
|
||||
@State private var searchText: String = ""
|
||||
|
||||
private let columns = [
|
||||
GridItem(.adaptive(minimum: 50, maximum: 60))
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: columns, spacing: 16) {
|
||||
ForEach(filteredSymbols, id: \.self) { symbol in
|
||||
Button {
|
||||
selection = symbol
|
||||
dismiss()
|
||||
} label: {
|
||||
VStack {
|
||||
Image(systemName: symbol)
|
||||
.font(.title2)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(selection == symbol ? Color.accentColor : Color.secondary.opacity(0.2))
|
||||
)
|
||||
.foregroundColor(selection == symbol ? .white : .primary)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.searchable(text: $searchText, prompt: "Search symbols")
|
||||
.navigationTitle("Choose Icon")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var filteredSymbols: [String] {
|
||||
if searchText.isEmpty {
|
||||
return Self.workoutSymbols
|
||||
}
|
||||
return Self.workoutSymbols.filter { $0.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
|
||||
// Curated list of workout/fitness-related SF Symbols
|
||||
static let workoutSymbols: [String] = [
|
||||
// Fitness & Exercise
|
||||
"dumbbell.fill",
|
||||
"dumbbell",
|
||||
"figure.strengthtraining.traditional",
|
||||
"figure.strengthtraining.functional",
|
||||
"figure.cross.training",
|
||||
"figure.core.training",
|
||||
"figure.cooldown",
|
||||
"figure.flexibility",
|
||||
"figure.pilates",
|
||||
"figure.yoga",
|
||||
"figure.highintensity.intervaltraining",
|
||||
"figure.mixed.cardio",
|
||||
"figure.rower",
|
||||
"figure.elliptical",
|
||||
"figure.stair.stepper",
|
||||
"figure.step.training",
|
||||
|
||||
// Running & Walking
|
||||
"figure.run",
|
||||
"figure.run.circle",
|
||||
"figure.run.circle.fill",
|
||||
"figure.walk",
|
||||
"figure.walk.circle",
|
||||
"figure.walk.circle.fill",
|
||||
"figure.hiking",
|
||||
"figure.outdoor.cycle",
|
||||
"figure.indoor.cycle",
|
||||
|
||||
// Sports
|
||||
"figure.boxing",
|
||||
"figure.kickboxing",
|
||||
"figure.martial.arts",
|
||||
"figure.wrestling",
|
||||
"figure.gymnastics",
|
||||
"figure.handball",
|
||||
"figure.basketball",
|
||||
"figure.tennis",
|
||||
"figure.badminton",
|
||||
"figure.racquetball",
|
||||
"figure.squash",
|
||||
"figure.volleyball",
|
||||
"figure.baseball",
|
||||
"figure.softball",
|
||||
"figure.golf",
|
||||
"figure.soccer",
|
||||
"figure.american.football",
|
||||
"figure.rugby",
|
||||
"figure.hockey",
|
||||
"figure.lacrosse",
|
||||
"figure.cricket",
|
||||
"figure.table.tennis",
|
||||
"figure.fencing",
|
||||
"figure.archery",
|
||||
"figure.bowling",
|
||||
"figure.disc.sports",
|
||||
|
||||
// Water Sports
|
||||
"figure.pool.swim",
|
||||
"figure.open.water.swim",
|
||||
"figure.surfing",
|
||||
"figure.waterpolo",
|
||||
"figure.rowing",
|
||||
"figure.sailing",
|
||||
"figure.fishing",
|
||||
|
||||
// Winter Sports
|
||||
"figure.skiing.downhill",
|
||||
"figure.skiing.crosscountry",
|
||||
"figure.snowboarding",
|
||||
"figure.skating",
|
||||
|
||||
// Climbing & Adventure
|
||||
"figure.climbing",
|
||||
"figure.equestrian.sports",
|
||||
"figure.hunting",
|
||||
|
||||
// Mind & Body
|
||||
"figure.mind.and.body",
|
||||
"figure.dance",
|
||||
"figure.barre",
|
||||
"figure.socialdance",
|
||||
"figure.australian.football",
|
||||
|
||||
// General Activity
|
||||
"figure.stand",
|
||||
"figure.wave",
|
||||
"figure.roll",
|
||||
"figure.jumprope",
|
||||
"figure.play",
|
||||
"figure.child",
|
||||
|
||||
// Health & Body
|
||||
"heart.fill",
|
||||
"heart",
|
||||
"heart.circle",
|
||||
"heart.circle.fill",
|
||||
"bolt.heart.fill",
|
||||
"bolt.heart",
|
||||
"waveform.path.ecg",
|
||||
"lungs.fill",
|
||||
"lungs",
|
||||
|
||||
// Energy & Power
|
||||
"bolt.fill",
|
||||
"bolt",
|
||||
"bolt.circle",
|
||||
"bolt.circle.fill",
|
||||
"flame.fill",
|
||||
"flame",
|
||||
"flame.circle",
|
||||
"flame.circle.fill",
|
||||
|
||||
// Timer & Tracking
|
||||
"stopwatch",
|
||||
"stopwatch.fill",
|
||||
"timer",
|
||||
"timer.circle",
|
||||
"timer.circle.fill",
|
||||
"clock",
|
||||
"clock.fill",
|
||||
|
||||
// Progress & Goals
|
||||
"trophy.fill",
|
||||
"trophy",
|
||||
"trophy.circle",
|
||||
"trophy.circle.fill",
|
||||
"medal.fill",
|
||||
"medal",
|
||||
"star.fill",
|
||||
"star",
|
||||
"star.circle",
|
||||
"star.circle.fill",
|
||||
"target",
|
||||
"scope",
|
||||
"chart.bar.fill",
|
||||
"chart.line.uptrend.xyaxis",
|
||||
"arrow.up.circle.fill",
|
||||
|
||||
// Misc
|
||||
"scalemass.fill",
|
||||
"scalemass",
|
||||
"bed.double.fill",
|
||||
"bed.double",
|
||||
"moon.fill",
|
||||
"moon",
|
||||
"sun.max.fill",
|
||||
"sun.max",
|
||||
"drop.fill",
|
||||
"drop",
|
||||
"leaf.fill",
|
||||
"leaf",
|
||||
"carrot.fill",
|
||||
"carrot",
|
||||
"fork.knife",
|
||||
"cup.and.saucer.fill",
|
||||
]
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SFSymbolPicker(selection: .constant("dumbbell.fill"))
|
||||
}
|
||||
Reference in New Issue
Block a user