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:
2026-01-19 06:42:15 -05:00
parent 2bfeb6a165
commit 13313a32d3
77 changed files with 3876 additions and 48 deletions

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