diff --git a/FitnessApp.xcodeproj/project.pbxproj b/FitnessApp.xcodeproj/project.pbxproj
index 1fca30c..1e106dd 100644
--- a/FitnessApp.xcodeproj/project.pbxproj
+++ b/FitnessApp.xcodeproj/project.pbxproj
@@ -393,12 +393,14 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = FitnessApp/FitnessApp.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"FitnessApp/Preview Content\"";
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHealthShareUsageDescription = "Please allow access to enjoy features of the app";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -422,12 +424,14 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = FitnessApp/FitnessApp.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"FitnessApp/Preview Content\"";
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHealthShareUsageDescription = "Please allow access to enjoy features of the app";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
diff --git a/FitnessApp.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/FitnessApp.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
new file mode 100644
index 0000000..97ee746
--- /dev/null
+++ b/FitnessApp.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
@@ -0,0 +1,6 @@
+
+
+
diff --git a/FitnessApp/FitnessApp.entitlements b/FitnessApp/FitnessApp.entitlements
new file mode 100644
index 0000000..e10f430
--- /dev/null
+++ b/FitnessApp/FitnessApp.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.developer.healthkit
+
+
+
diff --git a/FitnessApp/FitnessAppApp.swift b/FitnessApp/FitnessAppApp.swift
index cfad833..9d711bc 100644
--- a/FitnessApp/FitnessAppApp.swift
+++ b/FitnessApp/FitnessAppApp.swift
@@ -11,7 +11,7 @@ import SwiftUI
struct FitnessAppApp: App {
var body: some Scene {
WindowGroup {
- ContentView()
+ HomeView(viewModel: HomeViewModel())
}
}
}
diff --git a/FitnessApp/Home/ViewModels/HomeViewModel.swift b/FitnessApp/Home/ViewModels/HomeViewModel.swift
index 4511501..866b62a 100644
--- a/FitnessApp/Home/ViewModels/HomeViewModel.swift
+++ b/FitnessApp/Home/ViewModels/HomeViewModel.swift
@@ -8,11 +8,14 @@ import SwiftUI
@Observable
class HomeViewModel {
+
+ let healthManager = HealthManager.shared
+
var activities: [Activity] = []
var workouts: [Workout] = []
- var calories: Int = 123
- var active: Int = 205
- var stand: Int = 80
+ var calories: Int = 0
+ 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"),
@@ -27,4 +30,56 @@ class HomeViewModel {
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 {
+ do {
+ try await healthManager.requestHealthKitAccess()
+ fetchTodayStandHours()
+ fetchTodayCaloriesBurned()
+ fetchTodayExerciseTime()
+ } catch {
+ print(error.localizedDescription)
+ }
+ }
+
+ func fetchTodayCaloriesBurned() {
+ healthManager.fetchTodayCaloriesBurned() { result in
+ switch result {
+ case .success(let calories):
+ DispatchQueue.main.async {
+ self.calories = Int(calories)
+ }
+ case .failure(let failure):
+ print(failure.localizedDescription)
+ }
+ }
+ }
+
+ func fetchTodayExerciseTime() {
+ healthManager.fetchTodayExerciseTime() { result in
+ switch result {
+ case .success(let exercise):
+ DispatchQueue.main.async {
+ self.exercise = Int(exercise)
+ }
+ case .failure(let failure):
+ print(failure.localizedDescription)
+ }
+ }
+ }
+
+ func fetchTodayStandHours() {
+ healthManager.fetchTodayStandHours() { result in
+ switch result {
+ case .success(let hours):
+ DispatchQueue.main.async {
+ self.stand = Int(hours)
+ }
+ case .failure(let failure):
+ print(failure.localizedDescription)
+ }
+ }
+ }
+ }
}
diff --git a/FitnessApp/Home/Views/HomeView.swift b/FitnessApp/Home/Views/HomeView.swift
index f78ec7d..f233a28 100644
--- a/FitnessApp/Home/Views/HomeView.swift
+++ b/FitnessApp/Home/Views/HomeView.swift
@@ -21,7 +21,7 @@ struct HomeView: View {
HStack {
Spacer()
- VStack {
+ VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 8) {
Text("Calories")
.font(.callout)
@@ -32,18 +32,18 @@ struct HomeView: View {
.bold()
}.padding(.bottom)
- VStack {
- Text("Active")
+ VStack(alignment: .leading, spacing: 8) {
+ Text("Exercise")
.font(.callout)
.bold()
.foregroundColor(.green)
- Text("\(viewModel.active)")
+ Text("\(viewModel.exercise)")
.bold()
}.padding(.bottom)
- VStack {
+ VStack(alignment: .leading, spacing: 8) {
Text("Stand")
.font(.callout)
.bold()
@@ -60,7 +60,7 @@ struct HomeView: View {
ZStack {
Spacer()
ProgressCircleView(progress: $viewModel.calories, goal: 600, color: .red)
- ProgressCircleView(progress: $viewModel.active, goal: 600, color: .green).padding(.all, 20)
+ ProgressCircleView(progress: $viewModel.exercise, goal: 600, color: .green).padding(.all, 20)
ProgressCircleView(progress: $viewModel.stand, goal: 600, color: .blue).padding(.all, 40)
}
.padding(.horizontal)
diff --git a/FitnessApp/Managers/HealthManager.swift b/FitnessApp/Managers/HealthManager.swift
new file mode 100644
index 0000000..4033be3
--- /dev/null
+++ b/FitnessApp/Managers/HealthManager.swift
@@ -0,0 +1,122 @@
+//
+// HealthManager.swift
+// FitnessApp
+//
+// Created by Matt Bruce on 12/20/24.
+//
+
+import Foundation
+import HealthKit
+
+extension Date {
+ static var startOfDay: Date {
+ let calendar = Calendar.current
+ return calendar.startOfDay(for: Date())
+ }
+}
+
+class HealthManager {
+
+ static let shared = HealthManager()
+
+ let healthStore = HKHealthStore()
+
+ private init () {
+ Task {
+ do {
+ try await requestHealthKitAccess()
+ } catch {
+
+ }
+ }
+ }
+
+ 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)
+ }
+
+ func fetchTodayCaloriesBurned(completion: @escaping(Result) -> Void) {
+ guard HKHealthStore.isHealthDataAvailable() else {
+ completion(.failure(NSError(domain: "HealthManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "Health data unavailable"])))
+ return
+ }
+
+ let calories = HKQuantityType(.activeEnergyBurned)
+ let predicate = HKQuery.predicateForSamples(withStart: .startOfDay, end: Date())
+
+ let query = HKStatisticsQuery(quantityType: calories, 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))
+ } else {
+ completion(.failure(NSError(domain: "HealthManager", code: -3, userInfo: [NSLocalizedDescriptionKey: "No data found for today's calories"])))
+ }
+ }
+ 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
+ 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."])))
+ return
+ }
+
+ // Count stand hours
+ let standHours = samples.filter { $0.value == HKCategoryValueAppleStandHour.stood.rawValue }.count
+ completion(.success(Double(standHours)))
+ }
+
+ healthStore.execute(query)
+ }
+}
+