added healthkit
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
1df20c79cb
commit
0c5e3dae74
@ -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;
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "7A5F5A74-3D4A-401E-BAFF-43AF62FB9F72"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
</Bucket>
|
||||
8
FitnessApp/FitnessApp.entitlements
Normal file
8
FitnessApp/FitnessApp.entitlements
Normal 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>
|
||||
@ -11,7 +11,7 @@ import SwiftUI
|
||||
struct FitnessAppApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
HomeView(viewModel: HomeViewModel())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
122
FitnessApp/Managers/HealthManager.swift
Normal file
122
FitnessApp/Managers/HealthManager.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user