Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
Matt Bruce 2024-12-21 12:08:47 -06:00
parent 0c5e3dae74
commit ca2476be6f
7 changed files with 596 additions and 93 deletions

View File

@ -5,9 +5,10 @@
// Created by Matt Bruce on 12/20/24. // Created by Matt Bruce on 12/20/24.
// //
import SwiftUI import SwiftUI
import Foundation
struct Activity { struct Activity {
let id: Int let id = UUID()
let title: String let title: String
let subtitle: String let subtitle: String
let image: String let image: String

View File

@ -7,7 +7,7 @@
import SwiftUI import SwiftUI
struct Workout { struct Workout {
let id: Int let id = UUID()
let title: String let title: String
let image: String let image: String
let tintColor: Color let tintColor: Color

View File

@ -17,19 +17,19 @@ class HomeViewModel {
var exercise: Int = 0 var exercise: Int = 0
var stand: Int = 0 var stand: Int = 0
var mockActivities = [ // var mockActivities = [
Activity(id: 0, title: "Today Steps", subtitle: "10,000 steps", image: "figure.walk", tintColor: .green, amount: "9,812"), // Activity(id: 0, title: "Today Steps", subtitle: "10,000 steps", image: "figure.walk", tintColor: .green, amount: "9,812"),
Activity(id: 1, title: "Today Steps", subtitle: "1,000 steps", image: "figure.walk", tintColor: .blue, amount: "812"), // Activity(id: 1, title: "Today Steps", subtitle: "1,000 steps", image: "figure.walk", tintColor: .blue, amount: "812"),
Activity(id: 2, title: "Today Steps", subtitle: "12,000 steps", image: "figure.walk", tintColor: .purple, amount: "9,0000"), // Activity(id: 2, title: "Today Steps", subtitle: "12,000 steps", image: "figure.walk", tintColor: .purple, amount: "9,0000"),
Activity(id: 3, title: "Today Steps", subtitle: "50,000 steps", image: "figure.run", tintColor: .red, amount: "104,812") // Activity(id: 3, title: "Today Steps", subtitle: "50,000 steps", image: "figure.run", tintColor: .red, amount: "104,812")
] // ]
//
var mockWorkouts = [ // var mockWorkouts = [
Workout(id: 0, title: "Running", image: "figure.run", tintColor: .green, duration: "1 hrs", date: "Aug 3", calories: "100 kcal"), // Workout(id: 0, title: "Running", image: "figure.run", tintColor: .green, duration: "1 hrs", date: "Aug 3", calories: "100 kcal"),
Workout(id: 1, title: "Strength Training", image: "figure.run", tintColor: .purple, duration: "1.5 hrs", date: "Aug 3", calories: "130 kcal"), // Workout(id: 1, title: "Strength Training", image: "figure.run", tintColor: .purple, duration: "1.5 hrs", date: "Aug 3", calories: "130 kcal"),
Workout(id: 2, title: "Walking", image: "figure.run", tintColor: .blue, duration: ".5 hrs", date: "Aug 3", calories: "250 kcal"), // Workout(id: 2, title: "Walking", image: "figure.run", tintColor: .blue, duration: ".5 hrs", date: "Aug 3", calories: "250 kcal"),
Workout(id: 3, title: "Bike", image: "figure.run", tintColor: .red, duration: "2 hrs", date: "Aug 29", calories: "500 kcal") // Workout(id: 3, title: "Bike", image: "figure.run", tintColor: .red, duration: "2 hrs", date: "Aug 29", calories: "500 kcal")
] // ]
init() { init() {
Task { Task {
@ -38,6 +38,9 @@ class HomeViewModel {
fetchTodayStandHours() fetchTodayStandHours()
fetchTodayCaloriesBurned() fetchTodayCaloriesBurned()
fetchTodayExerciseTime() fetchTodayExerciseTime()
fetchTodaySteps()
fetchCurrentWeekActivities()
fetchCurrentMonthActivities()
} catch { } catch {
print(error.localizedDescription) print(error.localizedDescription)
} }
@ -48,7 +51,9 @@ class HomeViewModel {
switch result { switch result {
case .success(let calories): case .success(let calories):
DispatchQueue.main.async { DispatchQueue.main.async {
let activity = Activity(title: "Calories Burned", subtitle: "today", image: "flame", tintColor: .red, amount: calories.formattedNumberString())
self.calories = Int(calories) self.calories = Int(calories)
self.activities.append(activity)
} }
case .failure(let failure): case .failure(let failure):
print(failure.localizedDescription) print(failure.localizedDescription)
@ -81,5 +86,44 @@ class HomeViewModel {
} }
} }
} }
func fetchTodaySteps() {
healthManager.fetchTodaySteps() { result in
switch result {
case .success(let steps):
DispatchQueue.main.async {
self.activities.append(steps)
}
case .failure(let failure):
print(failure.localizedDescription)
}
}
}
func fetchCurrentWeekActivities() {
healthManager.fetchCurrentWeekWorkoutStats { result in
switch result {
case .success(let activities):
DispatchQueue.main.async {
self.activities.append(contentsOf: activities)
}
case .failure(let failure):
print(failure.localizedDescription)
}
}
}
func fetchCurrentMonthActivities() {
healthManager.fetchWorkoutsForMonth(month: Date()) { result in
switch result {
case .success(let activities):
DispatchQueue.main.async {
self.workouts.append(contentsOf: activities)
}
case .failure(let failure):
print(failure.localizedDescription)
}
}
}
} }
} }

View File

@ -37,8 +37,7 @@ struct ActivityCard: View {
} }
#Preview { #Preview {
ActivityCard(activity: .init(id: 1, ActivityCard(activity: .init(title: "Test",
title: "Test",
subtitle: "Goal 10,000", subtitle: "Goal 10,000",
image: "walk", image: "walk",
tintColor: .green, tintColor: .green,

View File

@ -87,14 +87,14 @@ struct HomeView: View {
} }
} }
.padding(.horizontal) .padding(.horizontal)
if !viewModel.activities.isEmpty {
LazyVGrid(columns: Array(repeating: GridItem(spacing: 20), count: 2)) { LazyVGrid(columns: Array(repeating: GridItem(spacing: 20), count: 2)) {
ForEach(viewModel.mockActivities , id: \.id) { activity in ForEach(viewModel.activities , id: \.id) { activity in
ActivityCard(activity: activity) ActivityCard(activity: activity)
} }
}.padding(.horizontal) }.padding(.horizontal)
}
//Recent Workouts //Recent Workouts
HStack { HStack {
Text("Recent Workouts") Text("Recent Workouts")
@ -114,7 +114,7 @@ struct HomeView: View {
.padding(.horizontal) .padding(.horizontal)
.padding(.top) .padding(.top)
LazyVStack{ LazyVStack{
ForEach(viewModel.mockWorkouts, id: \.id) { workout in ForEach(viewModel.workouts, id: \.id) { workout in
WorkoutCard(workout: workout) WorkoutCard(workout: workout)
} }
} }

View File

@ -39,5 +39,5 @@ struct WorkoutCard: View {
} }
#Preview { #Preview {
WorkoutCard(workout: .init(id: 0, title: "Running", image: "figure.run", tintColor: .green, duration: "1 hour", date: "Aug 3", calories: "100 kcal")) WorkoutCard(workout: .init(title: "Running", image: "figure.run", tintColor: .green, duration: "1 hour", date: "Aug 3", calories: "100 kcal"))
} }

View File

@ -4,7 +4,7 @@
// //
// Created by Matt Bruce on 12/20/24. // Created by Matt Bruce on 12/20/24.
// //
import SwiftUI
import Foundation import Foundation
import HealthKit import HealthKit
@ -13,110 +13,569 @@ extension Date {
let calendar = Calendar.current let calendar = Calendar.current
return calendar.startOfDay(for: Date()) return calendar.startOfDay(for: Date())
} }
static var startOfWeek: Date {
let calendar = Calendar.current
var components = calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date())
components.weekday = 2
return calendar.date(from: components) ?? Date()
}
} }
extension Double {
func formattedNumberString() -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 0
return formatter.string(from: NSNumber(value: self)) ?? "0"
}
}
/// A centralized manager for accessing HealthKit data
class HealthManager { class HealthManager {
static let shared = HealthManager() static let shared = HealthManager()
private let healthStore = HKHealthStore()
let healthStore = HKHealthStore() private init() {
private init () {
Task { Task {
do { do {
try await requestHealthKitAccess() try await requestHealthKitAccess()
} catch { } catch {
print("Failed to request HealthKit access: \(error.localizedDescription)")
} }
} }
} }
}
// MARK: - HealthKit Authorization
extension HealthManager {
/// Requests HealthKit access for necessary data types
func requestHealthKitAccess() async throws { func requestHealthKitAccess() async throws {
let calories = HKQuantityType(.activeEnergyBurned) let readTypes: Set<HKObjectType> = [
let exercise = HKQuantityType(.appleExerciseTime) HKQuantityType(.activeEnergyBurned),
let stand = HKCategoryType(.appleStandHour) HKQuantityType(.appleExerciseTime),
HKCategoryType(.appleStandHour),
let healthTypes: Set = [calories, exercise, stand] HKQuantityType(.stepCount),
HKQuantityType.workoutType()
try await healthStore.requestAuthorization(toShare: [], read: healthTypes) ]
try await healthStore.requestAuthorization(toShare: [], read: readTypes)
} }
func fetchTodayCaloriesBurned(completion: @escaping(Result<Double, Error>) -> Void) { /// Ensures HealthKit data is available on this device
private func ensureHealthDataAvailable() throws {
guard HKHealthStore.isHealthDataAvailable() else { guard HKHealthStore.isHealthDataAvailable() else {
completion(.failure(NSError(domain: "HealthManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "Health data unavailable"]))) throw HealthManagerError.healthDataUnavailable
return
} }
}
}
let calories = HKQuantityType(.activeEnergyBurned) // MARK: - Error Handling
let predicate = HKQuery.predicateForSamples(withStart: .startOfDay, end: Date()) extension HealthManager {
enum HealthManagerError: Error {
case healthDataUnavailable
case noDataFound(String)
var localizedDescription: String {
switch self {
case .healthDataUnavailable:
return "Health data is unavailable on this device."
case .noDataFound(let dataType):
return "No \(dataType) data found for today."
}
}
}
}
let query = HKStatisticsQuery(quantityType: calories, quantitySamplePredicate: predicate) { _, results, error in // MARK: - Query Execution
extension HealthManager {
/// Executes a statistics query and processes the result
private func executeStatisticsQuery(
quantityType: HKQuantityType,
predicate: NSPredicate,
unit: HKUnit,
completion: @escaping (Result<Double, Error>) -> Void
) {
let query = HKStatisticsQuery(quantityType: quantityType, quantitySamplePredicate: predicate) { _, results, error in
if let error = error { if let error = error {
completion(.failure(error)) completion(.failure(error))
return return
} }
if let quantity = results?.sumQuantity() { if let quantity = results?.sumQuantity() {
let caloriesBurned = quantity.doubleValue(for: .kilocalorie()) let value = quantity.doubleValue(for: unit)
completion(.success(caloriesBurned)) completion(.success(value))
} else { } else {
completion(.failure(NSError(domain: "HealthManager", code: -3, userInfo: [NSLocalizedDescriptionKey: "No data found for today's calories"]))) completion(.failure(HealthManagerError.noDataFound(quantityType.identifier)))
} }
} }
healthStore.execute(query) healthStore.execute(query)
} }
func fetchTodayExerciseTime(completion: @escaping(Result<Double, Error>) -> Void) { /// Executes a sample query and processes the result
guard HKHealthStore.isHealthDataAvailable() else { private func executeSampleQuery(
completion(.failure(NSError(domain: "HealthManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "Health data unavailable"]))) sampleType: HKSampleType,
return predicate: NSPredicate,
} completion: @escaping (Result<[HKSample], Error>) -> Void
) {
let exercise = HKQuantityType(.appleExerciseTime) let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, results, error in
let predicate = HKQuery.predicateForSamples(withStart: .startOfDay, end: Date())
let query = HKStatisticsQuery(quantityType: exercise, quantitySamplePredicate: predicate) { _, results, error in
if let error = error {
completion(.failure(error))
return
}
if let quantity = results?.sumQuantity() {
let exerciseTime = quantity.doubleValue(for: .minute())
completion(.success(exerciseTime))
} else {
completion(.failure(NSError(domain: "HealthManager", code: -3, userInfo: [NSLocalizedDescriptionKey: "No exercise time data found for today."])))
}
}
healthStore.execute(query)
}
func fetchTodayStandHours(completion: @escaping(Result<Double, Error>) -> Void) {
guard HKHealthStore.isHealthDataAvailable() else {
completion(.failure(NSError(domain: "HealthManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "Health data unavailable"])))
return
}
let stand = HKCategoryType(.appleStandHour)
let predicate = HKQuery.predicateForSamples(withStart: .startOfDay, end: Date())
let query = HKSampleQuery(sampleType: stand, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, results, error in
if let error = error { if let error = error {
completion(.failure(error)) completion(.failure(error))
return return
} }
guard let samples = results as? [HKCategorySample], !samples.isEmpty else { guard let samples = results else {
completion(.failure(NSError(domain: "HealthManager", code: -3, userInfo: [NSLocalizedDescriptionKey: "No stand data found for today."]))) completion(.failure(HealthManagerError.noDataFound(sampleType.identifier)))
return return
} }
// Count stand hours completion(.success(samples))
let standHours = samples.filter { $0.value == HKCategoryValueAppleStandHour.stood.rawValue }.count
completion(.success(Double(standHours)))
} }
healthStore.execute(query) healthStore.execute(query)
} }
} }
// MARK: - Fetch Methods
extension HealthManager {
/// Fetches the active calories burned today
func fetchTodayCaloriesBurned(completion: @escaping (Result<Double, Error>) -> Void) {
do {
try ensureHealthDataAvailable()
let quantityType = HKQuantityType(.activeEnergyBurned)
let predicate = HKQuery.predicateForSamples(withStart: .startOfDay, end: Date())
executeStatisticsQuery(quantityType: quantityType, predicate: predicate, unit: .kilocalorie(), completion: completion)
} catch {
completion(.failure(error))
}
}
/// Fetches the exercise time today
func fetchTodayExerciseTime(completion: @escaping (Result<Double, Error>) -> Void) {
do {
try ensureHealthDataAvailable()
let quantityType = HKQuantityType(.appleExerciseTime)
let predicate = HKQuery.predicateForSamples(withStart: .startOfDay, end: Date())
executeStatisticsQuery(quantityType: quantityType, predicate: predicate, unit: .minute(), completion: completion)
} catch {
completion(.failure(error))
}
}
/// Fetches the number of stand hours today
func fetchTodayStandHours(completion: @escaping (Result<Double, Error>) -> Void) {
do {
try ensureHealthDataAvailable()
let categoryType = HKCategoryType(.appleStandHour)
let predicate = HKQuery.predicateForSamples(withStart: .startOfDay, end: Date())
executeSampleQuery(sampleType: categoryType, predicate: predicate) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let samples):
// Ensure samples are cast to HKCategorySample
guard let categorySamples = samples as? [HKCategorySample] else {
completion(.failure(HealthManagerError.noDataFound("stand hours")))
return
}
// Count stand hours
let standHours = categorySamples.filter { $0.value == HKCategoryValueAppleStandHour.stood.rawValue }.count
completion(.success(Double(standHours)))
}
}
} catch {
completion(.failure(error))
}
}
func fetchTodaySteps(completion: @escaping (Result<Activity, Error>) -> Void) {
do {
try ensureHealthDataAvailable()
let quantityType = HKQuantityType(.stepCount)
let predicate = HKQuery.predicateForSamples(withStart: .startOfDay, end: Date(), options: .strictStartDate)
executeStatisticsQuery(quantityType: quantityType, predicate: predicate, unit: .count()) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let steps):
let activity = Activity(title: "Today Steps", subtitle: "Goal: 800", image: "figure.walk", tintColor: .green, amount: steps.formattedNumberString())
completion(.success(activity))
}
}
} catch {
completion(.failure(error))
}
}
func fetchCurrentWeekWorkoutStats(completion: @escaping (Result<[Activity], Error>) -> Void) {
do {
try ensureHealthDataAvailable()
let workoutType = HKObjectType.workoutType()
let predicate = HKQuery.predicateForSamples(withStart: .startOfWeek, end: Date())
executeSampleQuery(sampleType: workoutType, predicate: predicate) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let samples):
guard let workouts = samples as? [HKWorkout] else {
completion(.failure(HealthManagerError.noDataFound("workout stats for the current week")))
return
}
// Group and aggregate workouts by activity type
var aggregatedWorkouts: [HKWorkoutActivityType: (duration: Double, calories: Double)] = [:]
for workout in workouts {
let activityType = workout.workoutActivityType
let duration = workout.duration / 60 // Convert seconds to minutes
let calories = workout.totalEnergyBurned?.doubleValue(for: .kilocalorie()) ?? 0.0
if aggregatedWorkouts[activityType] == nil {
aggregatedWorkouts[activityType] = (duration: 0.0, calories: 0.0)
}
aggregatedWorkouts[activityType]?.duration += duration
aggregatedWorkouts[activityType]?.calories += calories
}
// Convert aggregated results into Activity objects
let activities: [Activity] = aggregatedWorkouts.map { activityType, totals in
let durationString = "\(totals.duration.formattedNumberString()) min"
let caloriesString = "\(totals.calories.formattedNumberString()) kcal"
return Activity(
title: activityType.readableName,
subtitle: "This week: \(durationString)",
image: activityType.image,
tintColor: activityType.tintColor,
amount: caloriesString
)
}
completion(.success(activities))
}
}
} catch {
completion(.failure(error))
}
}
func fetchWorkoutsForMonth(month: Date, completion: @escaping (Result<[Workout], Error>) -> Void) {
do {
try ensureHealthDataAvailable()
let workoutType = HKObjectType.workoutType()
// Calculate the start and end dates for the specified month
let calendar = Calendar.current
guard let startOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: month)),
let endOfMonth = calendar.date(byAdding: DateComponents(month: 1, day: -1), to: startOfMonth) else {
completion(.failure(HealthManagerError.noDataFound("Invalid month date range.")))
return
}
let predicate = HKQuery.predicateForSamples(withStart: startOfMonth, end: endOfMonth)
executeSampleQuery(sampleType: workoutType, predicate: predicate) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let samples):
guard let workouts = samples as? [HKWorkout] else {
completion(.failure(HealthManagerError.noDataFound("workouts for the specified month.")))
return
}
// Convert workouts into your Workout model (if needed)
let mappedWorkouts: [Workout] = workouts.map { workout in
Workout(title: workout.workoutActivityType.readableName,
image: workout.workoutActivityType.image,
tintColor: workout.workoutActivityType.tintColor,
duration: "\((workout.duration / 60).formattedNumberString())",
date: workout.startDate.formatted(date: .abbreviated, time: .omitted),
calories: "\((workout.totalEnergyBurned?.doubleValue(for: .kilocalorie()) ?? 0.0).formattedNumberString())")
}
completion(.success(mappedWorkouts))
}
}
} catch {
completion(.failure(error))
}
}
}
extension HKWorkoutActivityType {
var readableName: String {
switch self {
case .americanFootball: return "American Football"
case .archery: return "Archery"
case .australianFootball: return "Australian Football"
case .badminton: return "Badminton"
case .baseball: return "Baseball"
case .basketball: return "Basketball"
case .bowling: return "Bowling"
case .boxing: return "Boxing"
case .climbing: return "Climbing"
case .cricket: return "Cricket"
case .crossTraining: return "Cross Training"
case .curling: return "Curling"
case .cycling: return "Cycling"
case .dance: return "Dance"
case .elliptical: return "Elliptical"
case .equestrianSports: return "Equestrian Sports"
case .fencing: return "Fencing"
case .fishing: return "Fishing"
case .functionalStrengthTraining: return "Functional Strength Training"
case .golf: return "Golf"
case .gymnastics: return "Gymnastics"
case .handball: return "Handball"
case .hiking: return "Hiking"
case .hockey: return "Hockey"
case .lacrosse: return "Lacrosse"
case .martialArts: return "Martial Arts"
case .mindAndBody: return "Mind and Body"
case .paddleSports: return "Paddle Sports"
case .play: return "Play"
case .preparationAndRecovery: return "Preparation and Recovery"
case .racquetball: return "Racquetball"
case .rowing: return "Rowing"
case .rugby: return "Rugby"
case .running: return "Running"
case .sailing: return "Sailing"
case .skatingSports: return "Skating Sports"
case .snowSports: return "Snow Sports"
case .soccer: return "Soccer"
case .softball: return "Softball"
case .squash: return "Squash"
case .stairClimbing: return "Stair Climbing"
case .surfingSports: return "Surfing Sports"
case .swimming: return "Swimming"
case .tableTennis: return "Table Tennis"
case .tennis: return "Tennis"
case .trackAndField: return "Track and Field"
case .traditionalStrengthTraining: return "Traditional Strength Training"
case .volleyball: return "Volleyball"
case .walking: return "Walking"
case .waterFitness: return "Water Fitness"
case .waterPolo: return "Water Polo"
case .waterSports: return "Water Sports"
case .wrestling: return "Wrestling"
case .yoga: return "Yoga"
case .barre: return "Barre"
case .coreTraining: return "Core Training"
case .crossCountrySkiing: return "Cross Country Skiing"
case .downhillSkiing: return "Downhill Skiing"
case .flexibility: return "Flexibility"
case .highIntensityIntervalTraining: return "High-Intensity Interval Training (HIIT)"
case .jumpRope: return "Jump Rope"
case .kickboxing: return "Kickboxing"
case .pilates: return "Pilates"
case .snowboarding: return "Snowboarding"
case .stairs: return "Stairs"
case .stepTraining: return "Step Training"
case .wheelchairWalkPace: return "Wheelchair Walk Pace"
case .wheelchairRunPace: return "Wheelchair Run Pace"
case .taiChi: return "Tai Chi"
case .mixedCardio: return "Mixed Cardio"
case .handCycling: return "Hand Cycling"
case .discSports: return "Disc Sports"
case .fitnessGaming: return "Fitness Gaming"
case .danceInspiredTraining: return "Dance-Inspired Training"
case .hunting: return "Hunting"
case .mixedMetabolicCardioTraining: return "Mixed Metabolic Cardio Training"
case .cardioDance: return "Cardio Dance"
case .socialDance: return "Social Dance"
case .pickleball: return "Pickleball"
case .cooldown: return "Cooldown"
case .swimBikeRun: return "Swim Bike Run"
case .transition: return "Transition"
case .underwaterDiving: return "Underwater Diving"
case .other: return "Other"
@unknown default: return "Unknown"
}
}
var image: String {
switch self {
case .americanFootball: return "sportscourt.fill"
case .archery: return "scope"
case .australianFootball: return "sportscourt"
case .badminton: return "figure.badminton"
case .baseball: return "baseball.fill"
case .basketball: return "sportscourt"
case .bowling: return "bowl"
case .boxing: return "boxing.glove.fill"
case .climbing: return "figure.climbing"
case .cricket: return "figure.cricket"
case .crossTraining: return "figure.cross.training"
case .curling: return "curling.stone"
case .cycling: return "bicycle"
case .dance: return "figure.dance"
case .elliptical: return "figure.elliptical"
case .equestrianSports: return "figure.equestrian.sports"
case .fencing: return "swords"
case .fishing: return "fish"
case .functionalStrengthTraining: return "figure.strengthtraining.functional"
case .golf: return "flag"
case .gymnastics: return "figure.gymnastics"
case .handball: return "sportscourt"
case .hiking: return "figure.hiking"
case .hockey: return "figure.hockey"
case .lacrosse: return "figure.lacrosse"
case .martialArts: return "figure.martial.arts"
case .mindAndBody: return "figure.mind.and.body"
case .paddleSports: return "figure.paddle.sports"
case .play: return "figure.play"
case .preparationAndRecovery: return "figure.preparation.recovery"
case .racquetball: return "figure.racquetball"
case .rowing: return "figure.rowing"
case .rugby: return "figure.rugby"
case .running: return "figure.run"
case .sailing: return "sailboat.fill"
case .skatingSports: return "figure.skating"
case .snowSports: return "snowflake"
case .soccer: return "soccerball"
case .softball: return "softball.fill"
case .squash: return "figure.squash"
case .stairClimbing: return "figure.stair.climbing"
case .surfingSports: return "figure.surfing"
case .swimming: return "figure.swimming"
case .tableTennis: return "figure.table.tennis"
case .tennis: return "tennis.racket"
case .trackAndField: return "sportscourt"
case .traditionalStrengthTraining: return "figure.strengthtraining.traditional"
case .volleyball: return "figure.volleyball"
case .walking: return "figure.walk"
case .waterFitness: return "figure.water.fitness"
case .waterPolo: return "figure.water.polo"
case .waterSports: return "figure.water.sports"
case .wrestling: return "figure.wrestling"
case .yoga: return "figure.yoga"
case .barre: return "figure.barre"
case .coreTraining: return "figure.core.training"
case .crossCountrySkiing: return "figure.cross.country.skiing"
case .downhillSkiing: return "figure.downhill.skiing"
case .flexibility: return "figure.flexibility"
case .highIntensityIntervalTraining: return "figure.highintensity.intervaltraining"
case .jumpRope: return "figure.jump.rope"
case .kickboxing: return "figure.kickboxing"
case .pilates: return "figure.pilates"
case .snowboarding: return "figure.snowboarding"
case .stairs: return "figure.stairs"
case .stepTraining: return "figure.step.training"
case .wheelchairWalkPace: return "figure.wheelchair.walk"
case .wheelchairRunPace: return "figure.wheelchair.run"
case .taiChi: return "figure.taichi"
case .mixedCardio: return "figure.mixed.cardio"
case .handCycling: return "figure.hand.cycling"
case .discSports: return "figure.disc.sports"
case .fitnessGaming: return "figure.fitness.gaming"
case .danceInspiredTraining: return "figure.dance"
case .hunting: return "figure.hunting"
case .mixedMetabolicCardioTraining: return "figure.mixed.cardio"
case .cardioDance: return "figure.dance"
case .socialDance: return "figure.dance"
case .pickleball: return "figure.pickleball"
case .cooldown: return "figure.cooldown"
case .swimBikeRun: return "figure.swim.bike.run"
case .transition: return "figure.transition"
case .underwaterDiving: return "figure.underwater.diving"
case .other: return "ellipsis.circle"
@unknown default: return "questionmark.circle"
}
}
var tintColor: Color {
switch self {
case .americanFootball: return .brown
case .archery: return .blue
case .australianFootball: return .orange
case .badminton: return .green
case .baseball: return .blue
case .basketball: return .orange
case .bowling: return .purple
case .boxing: return .red
case .climbing: return .brown
case .cricket: return .green
case .crossTraining: return .yellow
case .curling: return .cyan
case .cycling: return .teal
case .dance: return .pink
case .elliptical: return .gray
case .equestrianSports: return .brown
case .fencing: return .gray
case .fishing: return .blue
case .functionalStrengthTraining: return .indigo
case .golf: return .green
case .gymnastics: return .purple
case .handball: return .red
case .hiking: return .green
case .hockey: return .indigo
case .lacrosse: return .pink
case .martialArts: return .red
case .mindAndBody: return .purple
case .paddleSports: return .blue
case .play: return .yellow
case .preparationAndRecovery: return .gray
case .racquetball: return .teal
case .rowing: return .blue
case .rugby: return .brown
case .running: return .red
case .sailing: return .blue
case .skatingSports: return .cyan
case .snowSports: return .white
case .soccer: return .green
case .softball: return .orange
case .squash: return .purple
case .stairClimbing: return .gray
case .surfingSports: return .blue
case .swimming: return .cyan
case .tableTennis: return .orange
case .tennis: return .green
case .trackAndField: return .yellow
case .traditionalStrengthTraining: return .gray
case .volleyball: return .orange
case .walking: return .green
case .waterFitness: return .blue
case .waterPolo: return .blue
case .waterSports: return .cyan
case .wrestling: return .red
case .yoga: return .purple
case .barre: return .pink
case .coreTraining: return .gray
case .crossCountrySkiing: return .cyan
case .downhillSkiing: return .white
case .flexibility: return .purple
case .highIntensityIntervalTraining: return .red
case .jumpRope: return .orange
case .kickboxing: return .red
case .pilates: return .purple
case .snowboarding: return .cyan
case .stairs: return .gray
case .stepTraining: return .orange
case .wheelchairWalkPace: return .green
case .wheelchairRunPace: return .red
case .taiChi: return .blue
case .mixedCardio: return .yellow
case .handCycling: return .teal
case .discSports: return .orange
case .fitnessGaming: return .purple
case .danceInspiredTraining: return .pink
case .hunting: return .brown
case .mixedMetabolicCardioTraining: return .yellow
case .cardioDance: return .pink
case .socialDance: return .pink
case .pickleball: return .green
case .cooldown: return .gray
case .swimBikeRun: return .blue
case .transition: return .gray
case .underwaterDiving: return .cyan
case .other: return .gray
@unknown default: return .gray
}
}
}