added healthkit
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
1df20c79cb
commit
0c5e3dae74
@ -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;
|
||||||
|
|||||||
@ -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 {
|
struct FitnessAppApp: App {
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
HomeView(viewModel: HomeViewModel())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
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