From ca2476be6fbc0b77f8a8973da20453fe7255ee6b Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sat, 21 Dec 2024 12:08:47 -0600 Subject: [PATCH] updated Signed-off-by: Matt Bruce --- FitnessApp/Home/Models/Activity.swift | 3 +- FitnessApp/Home/Models/Workout.swift | 2 +- .../Home/ViewModels/HomeViewModel.swift | 70 ++- FitnessApp/Home/Views/ActivityCard.swift | 3 +- FitnessApp/Home/Views/HomeView.swift | 18 +- FitnessApp/Home/Views/WorkoutCard.swift | 2 +- FitnessApp/Managers/HealthManager.swift | 591 ++++++++++++++++-- 7 files changed, 596 insertions(+), 93 deletions(-) diff --git a/FitnessApp/Home/Models/Activity.swift b/FitnessApp/Home/Models/Activity.swift index 2af0469..805e495 100644 --- a/FitnessApp/Home/Models/Activity.swift +++ b/FitnessApp/Home/Models/Activity.swift @@ -5,9 +5,10 @@ // Created by Matt Bruce on 12/20/24. // import SwiftUI +import Foundation struct Activity { - let id: Int + let id = UUID() let title: String let subtitle: String let image: String diff --git a/FitnessApp/Home/Models/Workout.swift b/FitnessApp/Home/Models/Workout.swift index bfed137..776a570 100644 --- a/FitnessApp/Home/Models/Workout.swift +++ b/FitnessApp/Home/Models/Workout.swift @@ -7,7 +7,7 @@ import SwiftUI struct Workout { - let id: Int + let id = UUID() let title: String let image: String let tintColor: Color diff --git a/FitnessApp/Home/ViewModels/HomeViewModel.swift b/FitnessApp/Home/ViewModels/HomeViewModel.swift index 866b62a..82c1c94 100644 --- a/FitnessApp/Home/ViewModels/HomeViewModel.swift +++ b/FitnessApp/Home/ViewModels/HomeViewModel.swift @@ -17,19 +17,19 @@ class HomeViewModel { var exercise: Int = 0 var stand: Int = 0 - var mockActivities = [ - 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: 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") - ] - - var mockWorkouts = [ - 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: 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") - ] +// var mockActivities = [ +// 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: 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") +// ] +// +// var mockWorkouts = [ +// 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: 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") +// ] init() { Task { @@ -38,6 +38,9 @@ class HomeViewModel { fetchTodayStandHours() fetchTodayCaloriesBurned() fetchTodayExerciseTime() + fetchTodaySteps() + fetchCurrentWeekActivities() + fetchCurrentMonthActivities() } catch { print(error.localizedDescription) } @@ -48,7 +51,9 @@ class HomeViewModel { switch result { case .success(let calories): DispatchQueue.main.async { + let activity = Activity(title: "Calories Burned", subtitle: "today", image: "flame", tintColor: .red, amount: calories.formattedNumberString()) self.calories = Int(calories) + self.activities.append(activity) } case .failure(let failure): 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) + } + } + } } } diff --git a/FitnessApp/Home/Views/ActivityCard.swift b/FitnessApp/Home/Views/ActivityCard.swift index 83936b2..c3f8543 100644 --- a/FitnessApp/Home/Views/ActivityCard.swift +++ b/FitnessApp/Home/Views/ActivityCard.swift @@ -37,8 +37,7 @@ struct ActivityCard: View { } #Preview { - ActivityCard(activity: .init(id: 1, - title: "Test", + ActivityCard(activity: .init(title: "Test", subtitle: "Goal 10,000", image: "walk", tintColor: .green, diff --git a/FitnessApp/Home/Views/HomeView.swift b/FitnessApp/Home/Views/HomeView.swift index f233a28..4308506 100644 --- a/FitnessApp/Home/Views/HomeView.swift +++ b/FitnessApp/Home/Views/HomeView.swift @@ -87,14 +87,14 @@ struct HomeView: View { } } .padding(.horizontal) - - LazyVGrid(columns: Array(repeating: GridItem(spacing: 20), count: 2)) { - ForEach(viewModel.mockActivities , id: \.id) { activity in - ActivityCard(activity: activity) - } - }.padding(.horizontal) - - + if !viewModel.activities.isEmpty { + LazyVGrid(columns: Array(repeating: GridItem(spacing: 20), count: 2)) { + ForEach(viewModel.activities , id: \.id) { activity in + ActivityCard(activity: activity) + } + }.padding(.horizontal) + } + //Recent Workouts HStack { Text("Recent Workouts") @@ -114,7 +114,7 @@ struct HomeView: View { .padding(.horizontal) .padding(.top) LazyVStack{ - ForEach(viewModel.mockWorkouts, id: \.id) { workout in + ForEach(viewModel.workouts, id: \.id) { workout in WorkoutCard(workout: workout) } } diff --git a/FitnessApp/Home/Views/WorkoutCard.swift b/FitnessApp/Home/Views/WorkoutCard.swift index 1a3100e..259fe8e 100644 --- a/FitnessApp/Home/Views/WorkoutCard.swift +++ b/FitnessApp/Home/Views/WorkoutCard.swift @@ -39,5 +39,5 @@ struct WorkoutCard: View { } #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")) } diff --git a/FitnessApp/Managers/HealthManager.swift b/FitnessApp/Managers/HealthManager.swift index 4033be3..a434056 100644 --- a/FitnessApp/Managers/HealthManager.swift +++ b/FitnessApp/Managers/HealthManager.swift @@ -4,7 +4,7 @@ // // Created by Matt Bruce on 12/20/24. // - +import SwiftUI import Foundation import HealthKit @@ -13,110 +13,569 @@ extension Date { let calendar = Calendar.current 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 { - static let shared = HealthManager() + private let healthStore = HKHealthStore() - let healthStore = HKHealthStore() - - private init () { + private init() { Task { do { try await requestHealthKitAccess() } 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 { - let calories = HKQuantityType(.activeEnergyBurned) - let exercise = HKQuantityType(.appleExerciseTime) - let stand = HKCategoryType(.appleStandHour) - - let healthTypes: Set = [calories, exercise, stand] - - try await healthStore.requestAuthorization(toShare: [], read: healthTypes) + let readTypes: Set = [ + HKQuantityType(.activeEnergyBurned), + HKQuantityType(.appleExerciseTime), + HKCategoryType(.appleStandHour), + HKQuantityType(.stepCount), + HKQuantityType.workoutType() + ] + try await healthStore.requestAuthorization(toShare: [], read: readTypes) } - - func fetchTodayCaloriesBurned(completion: @escaping(Result) -> Void) { + + /// Ensures HealthKit data is available on this device + private func ensureHealthDataAvailable() throws { guard HKHealthStore.isHealthDataAvailable() else { - completion(.failure(NSError(domain: "HealthManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "Health data unavailable"]))) - return + throw HealthManagerError.healthDataUnavailable } + } +} - let calories = HKQuantityType(.activeEnergyBurned) - let predicate = HKQuery.predicateForSamples(withStart: .startOfDay, end: Date()) +// MARK: - Error Handling +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) -> Void + ) { + let query = HKStatisticsQuery(quantityType: quantityType, quantitySamplePredicate: predicate) { _, results, error in if let error = error { completion(.failure(error)) return } - + if let quantity = results?.sumQuantity() { - let caloriesBurned = quantity.doubleValue(for: .kilocalorie()) - completion(.success(caloriesBurned)) + let value = quantity.doubleValue(for: unit) + completion(.success(value)) } 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) } - func fetchTodayExerciseTime(completion: @escaping(Result) -> Void) { - guard HKHealthStore.isHealthDataAvailable() else { - completion(.failure(NSError(domain: "HealthManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "Health data unavailable"]))) - return - } - - let exercise = HKQuantityType(.appleExerciseTime) - 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) -> 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 + /// Executes a sample query and processes the result + private func executeSampleQuery( + sampleType: HKSampleType, + predicate: NSPredicate, + completion: @escaping (Result<[HKSample], Error>) -> Void + ) { + let query = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, results, error in if let error = error { completion(.failure(error)) return } - guard let samples = results as? [HKCategorySample], !samples.isEmpty else { - completion(.failure(NSError(domain: "HealthManager", code: -3, userInfo: [NSLocalizedDescriptionKey: "No stand data found for today."]))) + guard let samples = results else { + completion(.failure(HealthManagerError.noDataFound(sampleType.identifier))) return } - // Count stand hours - let standHours = samples.filter { $0.value == HKCategoryValueAppleStandHour.stood.rawValue }.count - completion(.success(Double(standHours))) + completion(.success(samples)) } - healthStore.execute(query) } } +// MARK: - Fetch Methods +extension HealthManager { + /// Fetches the active calories burned today + func fetchTodayCaloriesBurned(completion: @escaping (Result) -> 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) -> 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) -> 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) -> 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 + } + } +}