added healthkit

Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
Matt Bruce 2024-12-21 10:31:16 -06:00
parent 1df20c79cb
commit 0c5e3dae74
7 changed files with 205 additions and 10 deletions

View File

@ -393,12 +393,14 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = FitnessApp/FitnessApp.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"FitnessApp/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"FitnessApp/Preview Content\"";
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHealthShareUsageDescription = "Please allow access to enjoy features of the app";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -422,12 +424,14 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = FitnessApp/FitnessApp.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"FitnessApp/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"FitnessApp/Preview Content\"";
DEVELOPMENT_TEAM = 6R7KLBPBLZ; DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHealthShareUsageDescription = "Please allow access to enjoy features of the app";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "7A5F5A74-3D4A-401E-BAFF-43AF62FB9F72"
type = "1"
version = "2.0">
</Bucket>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.healthkit</key>
<true/>
</dict>
</plist>

View File

@ -11,7 +11,7 @@ import SwiftUI
struct FitnessAppApp: App { struct FitnessAppApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() HomeView(viewModel: HomeViewModel())
} }
} }
} }

View File

@ -8,11 +8,14 @@ import SwiftUI
@Observable @Observable
class HomeViewModel { class HomeViewModel {
let healthManager = HealthManager.shared
var activities: [Activity] = [] var activities: [Activity] = []
var workouts: [Workout] = [] var workouts: [Workout] = []
var calories: Int = 123 var calories: Int = 0
var active: Int = 205 var exercise: Int = 0
var stand: Int = 80 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"),
@ -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: 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() {
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)
}
}
}
}
} }

View File

@ -21,7 +21,7 @@ struct HomeView: View {
HStack { HStack {
Spacer() Spacer()
VStack { VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("Calories") Text("Calories")
.font(.callout) .font(.callout)
@ -32,18 +32,18 @@ struct HomeView: View {
.bold() .bold()
}.padding(.bottom) }.padding(.bottom)
VStack { VStack(alignment: .leading, spacing: 8) {
Text("Active") Text("Exercise")
.font(.callout) .font(.callout)
.bold() .bold()
.foregroundColor(.green) .foregroundColor(.green)
Text("\(viewModel.active)") Text("\(viewModel.exercise)")
.bold() .bold()
}.padding(.bottom) }.padding(.bottom)
VStack { VStack(alignment: .leading, spacing: 8) {
Text("Stand") Text("Stand")
.font(.callout) .font(.callout)
.bold() .bold()
@ -60,7 +60,7 @@ struct HomeView: View {
ZStack { ZStack {
Spacer() Spacer()
ProgressCircleView(progress: $viewModel.calories, goal: 600, color: .red) 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) ProgressCircleView(progress: $viewModel.stand, goal: 600, color: .blue).padding(.all, 40)
} }
.padding(.horizontal) .padding(.horizontal)

View File

@ -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<Double, Error>) -> 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<Double, Error>) -> 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<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 {
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)
}
}