- 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
229 lines
5.9 KiB
Swift
229 lines
5.9 KiB
Swift
//
|
|
// 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"))
|
|
}
|