From 4a2577004e9c9600be06ecd9cbe3f81685f6bfe9 Mon Sep 17 00:00:00 2001 From: Navan Chauhan Date: Sun, 11 Feb 2024 06:44:35 -0700 Subject: feature complete --- iCUrHealth/AutoCallerSheet.swift | 35 +++++ iCUrHealth/ContentView.swift | 12 +- iCUrHealth/DataAtAGlance.swift | 285 +++++++++++++++++++++++++++++----- iCUrHealth/DetailedAnalysisView.swift | 20 ++- iCUrHealth/HealthData.swift | 66 +++++++- iCUrHealth/HomeView.swift | 192 +++++++++++++++++------ iCUrHealth/SettingsView.swift | 60 +++++++ iCUrHealth/UserCharts.swift | 158 ++++++++++++++----- 8 files changed, 692 insertions(+), 136 deletions(-) create mode 100644 iCUrHealth/AutoCallerSheet.swift create mode 100644 iCUrHealth/SettingsView.swift diff --git a/iCUrHealth/AutoCallerSheet.swift b/iCUrHealth/AutoCallerSheet.swift new file mode 100644 index 0000000..6bad0ec --- /dev/null +++ b/iCUrHealth/AutoCallerSheet.swift @@ -0,0 +1,35 @@ +// +// AutoCallerSheet.swift +// iCUrHealth +// +// Created by Navan Chauhan on 2/11/24. +// + +import SwiftUI + +struct AutoCallerSheet: View { + + @State private var helpNeeded: String + + init(helpNeeded: String = "") { + self.helpNeeded = helpNeeded + } + + var body: some View { + Form { + Section { + Text("Looks like something is wrong. Tell me a bit about your symptoms and I can go ahead and make the right appointment for you...") + TextField("Describe...", text: $helpNeeded, axis: .vertical) + .textFieldStyle(.roundedBorder) + .padding() + Button("Make Appointment") { + print("Making an appointment") + } + } + } + } +} + +#Preview { + AutoCallerSheet() +} diff --git a/iCUrHealth/ContentView.swift b/iCUrHealth/ContentView.swift index 9a85785..808dbb6 100644 --- a/iCUrHealth/ContentView.swift +++ b/iCUrHealth/ContentView.swift @@ -44,18 +44,18 @@ struct ContentView: View { TabView{ HomeView() .tabItem { - Image(systemName: "gear") + Image(systemName: "stethoscope") Text("Home") } - UserCharts() + DataAtAGlance() .tabItem { - Image(systemName: "chart.xyaxis.line") - Text("Charts") + Image(systemName: "lightbulb.max") + Text("Analysis") } - DataAtAGlance() + UserCharts() .tabItem { Image(systemName: "chart.xyaxis.line") - Text("Trends") + Text("Charts") } } diff --git a/iCUrHealth/DataAtAGlance.swift b/iCUrHealth/DataAtAGlance.swift index b610bf9..e8e1b5c 100644 --- a/iCUrHealth/DataAtAGlance.swift +++ b/iCUrHealth/DataAtAGlance.swift @@ -15,32 +15,121 @@ struct Analysis: Hashable { var rank: Int = 0 // Neutral = 0, Good = 1, Bad = -1 } +func getFormattedSeriesLabel(_ series: String)-> String { + switch series { + case "steps": + return "Steps" + case "activeEnergy": + return "Active Energy" + case "exerciseMinutes": + return "Exercise Minutes" + case "sleepHours": + return "Sleep Hours" + case "minutesInDaylight": + return "Minutes In Daylight" + case "bodyWeight": + return "Body Weight" + case "screenTimeSocialMedia": + return "Time Spent browsing Social Media" + case "screenTimeTotal": + return "Total Screen Time" + default: + return "" + } +} + +struct CorrelationEntry: Identifiable { + let series1: String + let series2: String + let pValue: Double + let id = UUID() + var pValueString: String { + String(format: "%.2f", pValue) // Format as needed + } + var formattedSeries1Label: String { + return getFormattedSeriesLabel(series1) + } + var formattedSeries2Label: String { + return getFormattedSeriesLabel(series2) + } +} + +func pearsonCorrelation(xs: [Double], ys: [Double]) -> Double? { + let sumX = xs.reduce(0, +) + let sumY = ys.reduce(0, +) + let sumXSquared = xs.map { $0 * $0 }.reduce(0, +) + let sumYSquared = ys.map { $0 * $0 }.reduce(0, +) + let sumXY = zip(xs, ys).map(*).reduce(0, +) + let n = Double(xs.count) + + let numerator = n * sumXY - sumX * sumY + let denominator = sqrt((n * sumXSquared - pow(sumX, 2)) * (n * sumYSquared - pow(sumY, 2))) + + // Check for division by zero + if denominator == 0 { + return nil + } + + return numerator / denominator +} + struct DataAtAGlance: View { @State var healthData: [HealthData] = [] @State var predictions: [Analysis] = [] + @State var correlations: [CorrelationEntry] = [] + @AppStorage("countZeroSleepAsNoSleep") var countZeroSleepAsNoSleep: Bool = false var body: some View { NavigationView { VStack { - List(predictions, id: \.self) { pred in - NavigationLink { - DetailedAnalysisView(healthData: self.healthData, prediction: pred) + List { + Section(header: Text("Trend Analysis"), footer: Text("Data synced from Apple Health")) { + ForEach(predictions, id: \.self) { pred in + NavigationLink { + DetailedAnalysisView(healthData: self.healthData, prediction: pred) + } + label: { + VStack { + HStack { + if let img = pred.image { + Image(systemName: img) + } + Text(pred.category).font(.callout) + Spacer() + }.padding(.bottom, 1) + Text(pred.prediction) + } + } + } } - label: { - VStack { - HStack { - if let img = pred.image { - Image(systemName: img) - } - Text(pred.category).font(.callout) - Spacer() - }.padding(.bottom, 1) - Text(pred.prediction) + Section(header: Text("Correlation Analysis"), footer: Text("The Pearson correlation coefficient ranges from -1 to 1, where -1 indicates a perfect negative linear relationship, 0 indicates no linear relationship, and 1 indicates a perfect positive linear relationship between two variables.")) { + ForEach(correlations) { correl in + HStack { + if correl.pValue > 0.8 { + Image(systemName: "chart.line.uptrend.xyaxis.circle.fill") + .foregroundStyle(.green) + } else if correl.pValue > 0.45 { + Image(systemName: "chart.line.uptrend.xyaxis.circle.fill") + .foregroundStyle(.orange) + } else if correl.pValue < -0.8 { + Image(systemName: "chart.line.downtrend.xyaxis.circle.fill") + .foregroundStyle(.red) + } else if correl.pValue < -0.45 { + Image(systemName: "chart.line.downtrend.xyaxis.circle.fill") + .foregroundStyle(.orange) + } else { + Image(systemName: "chart.line.flattrend.xyaxis.circle.fill") + } + Text("\(correl.formattedSeries1Label) & \(correl.formattedSeries2Label) r = \(correl.pValueString)") + Spacer() + } + } } - } }.listRowSpacing(10) + + Text("Data for last \(healthData.count) days") .onAppear { let healthDataFetcher = HealthDataFetcher() @@ -102,39 +191,52 @@ struct DataAtAGlance: View { final_count = 0 for myValue in Array(initial) { - if myValue.sleepHours != nil { + if myValue.sleepHours != nil && myValue.sleepHours! != 0 { initial_total += Int(myValue.sleepHours!) initial_count += 1 + } else { + if countZeroSleepAsNoSleep { + initial_count += 1 + } } } for myValue in Array(recent) { - if myValue.sleepHours != nil { + if myValue.sleepHours != nil && myValue.sleepHours! != 0 { final_total += Int(myValue.sleepHours!) final_count += 1 + } else { + if countZeroSleepAsNoSleep { + final_count += 1 + } } } - - init_avg = max(initial_total / initial_count,1) - rece_avg = final_total / final_count - - percentage = rece_avg*100/init_avg - - - pred = Analysis(image: "bed.double", prediction: "", category: "Sleep") - if abs(percentage-100) > 5 { - if (percentage-100) > 0 { - pred.prediction = "Your sleep average in the last 7 days has been higher compared to the week before by (\(percentage-100))%" + if initial_total == 0 || final_total == 0 { + + } else + { + print("What is happening", initial_total, final_total, initial_count, final_count) + init_avg = initial_total / initial_count + rece_avg = final_total / final_count + + percentage = rece_avg*100/init_avg + + + pred = Analysis(image: "bed.double", prediction: "", category: "Sleep") + if abs(percentage-100) > 5 { + if (percentage-100) > 0 { + pred.prediction = "Your sleep average in the last 7 days has been higher compared to the week before by (\(percentage-100))%" + } else { + pred.prediction = "You have been sleeping \(init_avg - rece_avg) hours fewer compared to last week" + pred.rank = -1 + } } else { - pred.prediction = "You have been sleeping \(init_avg - rece_avg) hours fewer compared to last week" - pred.rank = -1 + pred.prediction = "Your sleep average in the last 7 days is relatively simimlar compared to the week before." } - } else { - pred.prediction = "Your sleep average in the last 7 days is relatively simimlar compared to the week before." + + self.predictions.append(pred) } - self.predictions.append(pred) - // Exercise Minutes initial_total = 0 @@ -145,7 +247,7 @@ struct DataAtAGlance: View { initial_total += Int(myValue.exerciseMinutes!) initial_count += 1 } else { - initial_count += 1 + initial_count += 0 } } @@ -154,9 +256,8 @@ struct DataAtAGlance: View { initial_total += Int(myValue.sleepHours!) initial_count += 1 } else { - initial_count += 1 + initial_count += 0 } - print(myValue.date) } init_avg = initial_total / initial_count @@ -173,10 +274,120 @@ struct DataAtAGlance: View { self.predictions.append(pred) // END + + // Screen Time + + initial_total = 0 + initial_count = 0 + + final_total = 0 + final_count = 0 + + for myValue in Array(initial) { + if myValue.screenTimeTotal != nil { + initial_total += Int(myValue.screenTimeTotal!) + initial_count += 1 + } + } + + for myValue in Array(recent) { + if myValue.screenTimeTotal != nil { + final_total += Int(myValue.screenTimeTotal!) + final_count += 1 + } + } + + init_avg = max(initial_total / initial_count,1) + rece_avg = final_total / final_count + + percentage = rece_avg*100/init_avg + + pred = Analysis(image: "iphone", prediction: "", category: "Screen Time") + if abs(percentage-100) > 5 { + if (percentage-100) > 0 { + pred.prediction = "Your screen time in the last 7 days has been higher compared to the week before by (\(percentage-100))%" + } else { + pred.prediction = "You have been using your phone \(init_avg - rece_avg) hours fewer compared to last week" + pred.rank = -1 + } + } else { + pred.prediction = "Your screen time in the last 7 days is relatively similar compared to the week before." + } + + self.predictions.append(pred) + + // END + + let propertyNames = ["steps", "activeEnergy", "exerciseMinutes", "sleepHours", "minutesInDaylight", "bodyWeight", "screenTimeSocialMedia", "screenTimeTotal"] + var correlationEntries: [CorrelationEntry] = [] + + for i in 0.. = \HealthData.steps // default initialization + var series2KeyPath: KeyPath = \HealthData.activeEnergy // default initialization + + switch propertyNames[i] { + case "steps": + series1KeyPath = \HealthData.steps + case "activeEnergy": + series1KeyPath = \HealthData.activeEnergy + case "exerciseMinutes": + series1KeyPath = \HealthData.exerciseMinutes + case "sleepHours": + series1KeyPath = \HealthData.sleepHours + case "minutesInDaylight": + series1KeyPath = \HealthData.minutesInDaylight + case "screenTimeTotal": + series1KeyPath = \HealthData.screenTimeTotal + case "screenTimeSocialMedia": + series1KeyPath = \HealthData.screenTimeSocialMedia + case "bodyWeight": + series1KeyPath = \HealthData.bodyWeight + default: + break + } + + switch propertyNames[j] { + case "steps": + series2KeyPath = \HealthData.steps + case "activeEnergy": + series2KeyPath = \HealthData.activeEnergy + case "exerciseMinutes": + series2KeyPath = \HealthData.exerciseMinutes + case "sleepHours": + series2KeyPath = \HealthData.sleepHours + case "minutesInDaylight": + series2KeyPath = \HealthData.minutesInDaylight + case "bodyWeight": + series2KeyPath = \HealthData.bodyWeight + case "screenTimeTotal": + series2KeyPath = \HealthData.screenTimeTotal + case "screenTimeSocialMedia": + series2KeyPath = \HealthData.screenTimeSocialMedia + default: + break + } + + // Now you can use series1KeyPath and series2KeyPath to access the properties dynamically + let filteredData = healthData.filter { $0[keyPath: series1KeyPath] != nil && $0[keyPath: series2KeyPath] != nil } + let xs = filteredData.compactMap { $0[keyPath: series1KeyPath] } + let ys = filteredData.compactMap { $0[keyPath: series2KeyPath] } + + // Calculate Pearson correlation coefficient + if let correlation = pearsonCorrelation(xs: xs, ys: ys) { + let entry = CorrelationEntry(series1: propertyNames[i], series2: propertyNames[j], pValue: correlation) + correlationEntries.append(entry) + } + } + } + + self.correlations = correlationEntries + + } } } - .navigationTitle("Trend Analysis") + .navigationTitle("Analysis") } } } diff --git a/iCUrHealth/DetailedAnalysisView.swift b/iCUrHealth/DetailedAnalysisView.swift index e13969c..448b115 100644 --- a/iCUrHealth/DetailedAnalysisView.swift +++ b/iCUrHealth/DetailedAnalysisView.swift @@ -13,7 +13,7 @@ struct DetailedAnalysisView: View { @State var healthData: [HealthData] @State var llmInput: String = "" let prediction: Analysis - + @State private var showHelpSheet: Bool = false let dateFormatter = DateFormatter() @@ -36,6 +36,10 @@ struct DetailedAnalysisView: View { if let exerMinutes = data.exerciseMinutes { BarMark(x: .value("Date", data.date), y: .value("Exercise Minutes", exerMinutes)) } + } else if prediction.category == "Screen Time" { + if let screenTimeTotal = data.screenTimeTotal { + BarMark(x: .value("Screen Time", data.date), y: .value("Screen Time (in hrs)", screenTimeTotal)) + } } } }.frame(height: 250) @@ -46,12 +50,24 @@ struct DetailedAnalysisView: View { Text("Taking regular steps, such as walking, is fundamental for maintaining physical health. It enhances cardiovascular fitness, aiding in the reduction of heart disease risk, and supports the management of body weight by burning calories. Engaging in regular walking can also strengthen bones and muscles, reducing the risk of osteoporosis and muscle loss. Additionally, it can improve mental health by reducing stress, anxiety, and depressive symptoms, contributing to an overall sense of well-being.") } else if prediction.category == "Exercise Minutes" { Text("Regular exercise, even in short durations, is highly beneficial for health. Just a few minutes of physical activity each day can boost cardiovascular health, improving heart function and reducing the risk of heart disease. These exercise minutes can also aid in weight management by increasing metabolic rate and burning extra calories. Furthermore, engaging in daily physical activity, even briefly, can enhance mental health by releasing endorphins that reduce stress and improve mood.") + } else if prediction.category == "Screen Time" { + Text("Excessive screen time can have detrimental effects on physical and mental health. Prolonged exposure to screens, particularly for activities like gaming or social media, can lead to sedentary lifestyles, eye strain, disrupted sleep patterns, and increased risk of obesity. Furthermore, excessive screen time may contribute to social isolation, diminished attention span, and impaired cognitive development, especially in children and adolescents. Balancing screen time with other activities is crucial for overall well-being.") } if prediction.rank == -1 { if prediction.category == "Sleep" { Text("It looks like you have not been sleeping well this week. Has something changed?") - TextField("Response", text: $llmInput) + } else { + Text("This is something you should be working to improve!") + } + Button("Book an appointment") { + showHelpSheet = true + }.sheet(isPresented: $showHelpSheet) { + if prediction.category == "Sleep" { + AutoCallerSheet(helpNeeded: "I have not been sleeping properly the past few days because...") + } else { + AutoCallerSheet() + } } } diff --git a/iCUrHealth/HealthData.swift b/iCUrHealth/HealthData.swift index 1eebec2..677a8c0 100644 --- a/iCUrHealth/HealthData.swift +++ b/iCUrHealth/HealthData.swift @@ -24,6 +24,11 @@ extension Date { } } +struct ScreenTimeData: Decodable { + let screenTimeTotal: [Double] + let screenTimeSocial: [Double] +} + struct HealthData: Codable, Identifiable { var id = UUID() var date: Date @@ -32,7 +37,9 @@ struct HealthData: Codable, Identifiable { var exerciseMinutes: Double? var bodyWeight: Double? var sleepHours: Double? - var heartRate: Double? + var minutesInDaylight: Double? + var screenTimeSocialMedia: Double? + var screenTimeTotal: Double? } enum HealthDataFetcherError: Error { @@ -54,7 +61,9 @@ class HealthDataFetcher { HKQuantityType(.appleExerciseTime), HKQuantityType(.bodyMass), HKQuantityType(.heartRate), - HKCategoryType(.sleepAnalysis) + HKCategoryType(.sleepAnalysis), + HKQuantityType(.timeInDaylight), + HKQuantityType(.restingHeartRate) ] try await healthStore.requestAuthorization(toShare: Set(), read: types) @@ -108,6 +117,10 @@ class HealthDataFetcher { options: [.cumulativeSum] ) } + + func fetchLastTwoWeeksTimeinDaylight() async throws -> [Double] { + try await fetchLastTwoWeeksQuantityData(for: .timeInDaylight, unit: HKUnit.minute(), options: [.cumulativeSum]) + } func fetchLastTwoWeeksActiveEnergy() async throws -> [Double] { try await fetchLastTwoWeeksQuantityData( @@ -216,12 +229,57 @@ extension HealthDataFetcher { async let caloriesBurned = fetchLastTwoWeeksActiveEnergy() async let exerciseTime = fetchLastTwoWeeksExerciseTime() async let bodyMass = fetchLastTwoWeeksBodyWeight() + async let daylightMinutes = fetchLastTwoWeeksTimeinDaylight() let fetchedStepCounts = try? await stepCounts let fetchedSleepHours = try? await sleepHours let fetchedCaloriesBurned = try? await caloriesBurned let fetchedExerciseTime = try? await exerciseTime let fetchedBodyMass = try? await bodyMass + let fetchedDaylightMinutes = try? await daylightMinutes + + var screenTimeTotal: [Double] = [5.26, 5.11, 3.38,5.38,5.12,6.18,6.28,7.5,5.37,5.29,5.19,5.1,6.12,8.25] + var screenTimeSocial: [Double] = [1.08,1.48,1.23,2.44,2.31,2.25,2.56,2.47,2.31,2.39,2.27,2.25,2.33,1.06] + + if let urlString = UserDefaults.standard.string(forKey: "screentimeConsumptionEndpoint"), + let url = URL(string: urlString) { + let semaphore = DispatchSemaphore(value: 0) + var request = URLRequest(url: url) + request.httpMethod = "GET" + var responseData: Data? + var responseError: Error? + let task = URLSession.shared.dataTask(with: request) { (data, response, error) in + responseData = data + responseError = error + semaphore.signal() // Signal that the task is completed + } + task.resume() + semaphore.wait() + if let error = responseError { + print("Error fetching data: \(error.localizedDescription)") + } else if let data = responseData { + do { + let decoder = JSONDecoder() + let screenTimeData = try decoder.decode(ScreenTimeData.self, from: data) + + // Access the fetched data + screenTimeTotal = screenTimeData.screenTimeTotal + screenTimeSocial = screenTimeData.screenTimeSocial + + // Use the variables as needed + print("Total Screen Time: \(screenTimeTotal)") + print("Social Screen Time: \(screenTimeSocial)") + } catch { + print("Error decoding JSON: \(error.localizedDescription)") + } + } else { + print("No data received") + } + } else { + print("URL not found in UserDefaults") + } + + for day in 0...13 { healthData[day].steps = fetchedStepCounts?[day] @@ -229,7 +287,11 @@ extension HealthDataFetcher { healthData[day].activeEnergy = fetchedCaloriesBurned?[day] healthData[day].exerciseMinutes = fetchedExerciseTime?[day] healthData[day].bodyWeight = fetchedBodyMass?[day] + healthData[day].minutesInDaylight = fetchedDaylightMinutes?[day] + healthData[day].screenTimeTotal = screenTimeTotal[day] + healthData[day].screenTimeSocialMedia = screenTimeSocial[day] } + return healthData } diff --git a/iCUrHealth/HomeView.swift b/iCUrHealth/HomeView.swift index c29758a..f72ef9b 100644 --- a/iCUrHealth/HomeView.swift +++ b/iCUrHealth/HomeView.swift @@ -13,6 +13,14 @@ import Charts struct HomeView: View { @State var authenticated = false @State var trigger = false + @State private var showingSettingsSheet: Bool = false + @State private var showingSomethingIsWrongSheet: Bool = false + @AppStorage("nursePhone") var nursePhone: String = "+13034925101" + @State private var hasBikingWorkouts: Bool = false + + @AppStorage("trackSkiing") var trackSkiing: Bool = true + @AppStorage("trackCycling") var trackCycling: Bool = true + @AppStorage("defaultChart") var defaultChart: String = "Steps" @StateObject private var viewModel = WorkoutViewModel() @@ -22,8 +30,32 @@ struct HomeView: View { var body: some View { NavigationView { List { - HStack { - Text("Steps") + Button(action: { + let telephone = "tel://" + guard let url = URL(string: telephone + nursePhone) else { + return + } + UIApplication.shared.open(url) + }, label: { + Label("Call Nurse Helpline", systemImage: "cross.case.circle") + }) + + + Button(action: { + showingSomethingIsWrongSheet = true + }, label: { + Label("Auto-Book an Appointment", systemImage: "phone.connection") + }).sheet(isPresented: $showingSomethingIsWrongSheet) { + AutoCallerSheet() + } + Button(action: { + print("Request") + }, label: { + Label("Request a call back", systemImage: "phone.arrow.down.left.fill") + }) + + VStack { + Text(defaultChart) Chart(data) { BarMark(x: .value("Date", $0.dateInterval), y: .value("Count", $0.data) @@ -31,58 +63,83 @@ struct HomeView: View { } }.frame(maxHeight: 100) + + if (trackSkiing) { - Button(action: { - Task { - try await fetchStepCountData() - } - }) - { - Text("Exp Function") - } - if !viewModel.workoutRouteCoordinates.isEmpty { - VStack { - HStack { - Text("Latest Downhill Skiing Workout").font(.callout) - Spacer() - } - MapView(route: viewModel.workoutRoute!) - .frame(height: 300) - HStack { - VStack { - Text("Top Speed") - Text("\(viewModel.workout!.totalDistance!)") + if !viewModel.workoutRouteCoordinates.isEmpty { + VStack { + HStack { + VStack { + Text("Latest Downhill Skiing Workout On \(viewModel.workout!.startDate)") + } + Spacer() } - } - } - } else { - Text("Fetching workout route...") - .onAppear { - if HKHealthStore.isHealthDataAvailable() { - trigger.toggle() + MapView(route: viewModel.workoutRoute!) + .frame(height: 300) + HStack { + VStack { + Text("Total Duration") + Text("\((viewModel.workout!.duration*100/3600).rounded()/100, specifier: "%.2f") hr") + } + VStack { + Text("Total Distance") + Text("\(viewModel.workout!.totalDistance!)") + } } - viewModel.fetchAndProcessWorkoutRoute() } - .healthDataAccessRequest(store: healthStore, - readTypes: allTypes, - trigger: trigger) { result in - switch result { - - case .success(_): - authenticated = true - Task { - try await fetchStepCountData() + } else { + Text("Fetching workout route...") + .onAppear { + if HKHealthStore.isHealthDataAvailable() { + trigger.toggle() + Task { + try await fetchStepCountData() + } + } + viewModel.fetchAndProcessWorkoutRoute() + } + .healthDataAccessRequest(store: healthStore, + readTypes: allTypes, + trigger: trigger) { result in + switch result { + + case .success(_): + authenticated = true + + case .failure(let error): + // Handle the error here. + fatalError("*** An error occurred while requesting authentication: \(error) ***") } - case .failure(let error): - // Handle the error here. - fatalError("*** An error occurred while requesting authentication: \(error) ***") } + } + } + + if (trackCycling) { + if (hasBikingWorkouts) { + + } else { + VStack { + Text("You have not completed any mountain biking workouts recently. Is everything alright?") } + } } }.listRowSpacing(10) .navigationTitle("iCUrHealth") - } + .toolbar { + Button(action: { + showingSettingsSheet = true + }, label: { + Label("Settings", systemImage: "gear").labelStyle(.iconOnly) + }) + } + }.sheet(isPresented: $showingSettingsSheet, onDismiss: { + Task { + try await fetchStepCountData() + } + }, content: { + SettingsView() + }) } private func experimentWithSkiWorkout() async throws { @@ -103,7 +160,36 @@ struct HomeView: View { } } + func checkForCyclingWorkouts(completion: @escaping ([HKWorkout]?) -> Void) { + let cyclingPredicate = HKQuery.predicateForWorkouts(with: .cycling) + + // Create a predicate to select workouts in the last 5 days + let calendar = Calendar.current + let endDate = Date() + guard let startDate = calendar.date(byAdding: .day, value: -5, to: endDate) else { return completion(nil) } + let datePredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate) + + // Combine the predicates + let compound = NSCompoundPredicate(andPredicateWithSubpredicates: [cyclingPredicate, datePredicate]) + + // Create the query + let query = HKSampleQuery(sampleType: HKObjectType.workoutType(), predicate: compound, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { (query, samples, error) in + DispatchQueue.main.async { + guard let workouts = samples as? [HKWorkout], error == nil else { + completion(nil) + return + } + hasBikingWorkouts = false + completion(workouts) + } + } + + healthStore.execute(query) + } + private func fetchStepCountData() async throws { + checkForCyclingWorkouts() { workouts in + } let calendar = Calendar(identifier: .gregorian) let today = calendar.startOfDay(for: Date()) @@ -120,9 +206,23 @@ struct HomeView: View { let thisWeek = HKQuery.predicateForSamples(withStart: startDate, end: endDate) - + var stepType = HKQuantityType(.stepCount) + var quantityUnit = HKUnit.count() // Create the query descriptor. - let stepType = HKQuantityType(.stepCount) + switch (defaultChart) { + case "Steps": + stepType = HKQuantityType(.stepCount) + quantityUnit = HKUnit.count() + case "Calories Burned": + stepType = HKQuantityType(.activeEnergyBurned) + quantityUnit = HKUnit.largeCalorie() + case "Exercise Minutes": + stepType = HKQuantityType(.appleExerciseTime) + quantityUnit = HKUnit.minute() + default: + return + } + let stepsThisWeek = HKSamplePredicate.quantitySample(type: stepType, predicate:thisWeek) let everyDay = DateComponents(day:1) @@ -142,7 +242,7 @@ struct HomeView: View { if let quantity = stats.sumQuantity() { //print(quantity, stats.startDate) dailyData.append( - chartData(tag: "activity", dateInterval: stats.startDate, data: quantity.doubleValue(for: HKUnit.count())) + chartData(tag: "activity", dateInterval: stats.startDate, data: quantity.doubleValue(for: quantityUnit)) ) } else { } diff --git a/iCUrHealth/SettingsView.swift b/iCUrHealth/SettingsView.swift new file mode 100644 index 0000000..9bef80c --- /dev/null +++ b/iCUrHealth/SettingsView.swift @@ -0,0 +1,60 @@ +// +// SettingsView.swift +// iCUrHealth +// +// Created by Navan Chauhan on 2/11/24. +// + +import SwiftUI +import iPhoneNumberField + +struct SettingsView: View { + + @AppStorage("nursePhone") var nursePhone: String = "+13034925101" + @AppStorage("fullName") var fullName: String = "John Doe" + @AppStorage("studentID") var studentID: String = "54329" + @AppStorage("dateOfBirth") var dateOfBirth: String = "2002-01-15" + @AppStorage("screentimeConsumptionEndpoint") var screentimeAPIEndpoint: String = "https://gist.githubusercontent.com/navanchauhan/74b3c4c7f3e9d94bf1500ce0a813bc3b/raw/f2c890366db6d2cf695a2049a50d5b91de01cb08/navan.json" + @AppStorage("countZeroSleepAsNoSleep") var countZeroSleepAsNoSleep: Bool = false + @AppStorage("trackSkiing") var trackSkiing: Bool = true + @AppStorage("trackCycling") var trackCycling: Bool = true + @AppStorage("defaultChart") var defaultChart: String = "Steps" + + var body: some View { + NavigationStack { + Form { + Section(header: Text("Medical Details")) { + HStack { + Text("Nurse Helpine") + iPhoneNumberField("Nurse Phoneline", text: $nursePhone) + } + TextField("Full Name", text: $fullName) + TextField("Student ID", text: $studentID) + TextField("Date of Birth", text: $dateOfBirth) + + } + + Section(header: Text("Dashboard Customization")) { + Toggle(isOn: $trackSkiing) { + Text("Track Skiing Workouts") + } + Toggle(isOn: $trackCycling) { + Text("Track Mountain Biking Workouts") + } + Picker("Default Chart", selection: $defaultChart) { + Text("Steps").tag("Steps") + Text("Calories Burned").tag("Calories Burned") + Text("Exercise Minutes").tag("Exercise Minutes") + } + } + + Section(header: Text("Advance Settings")) { + TextField("ScreenTime Consumption Endpoint", text: $screentimeAPIEndpoint) + Toggle(isOn: $countZeroSleepAsNoSleep) { + Text("Count 0 hours of sleep as no sleep") + } + } + } + } + } +} diff --git a/iCUrHealth/UserCharts.swift b/iCUrHealth/UserCharts.swift index 0fdf9d5..162bcfb 100644 --- a/iCUrHealth/UserCharts.swift +++ b/iCUrHealth/UserCharts.swift @@ -26,20 +26,20 @@ struct userChart: Identifiable { } -func getAverage(healthData: [HealthData]) -> Double { +func getAverage(healthData: [HealthData], valueForKey: (HealthData) -> Double?) -> Double { var total = 0.0 var count = 0.0 + for data in healthData { - if let stepCount = data.steps { - total += Double(stepCount) // Assuming stepCount is an Int + if let value = valueForKey(data) { + if value != 0.0 { + total += value count += 1 } } - if count > 0 { - return total / count - } else { - return 0 // Return 0 to avoid division by zero if there are no step counts - } + } + + return count > 0 ? total / count : 0 } struct UserCharts: View { @@ -47,47 +47,119 @@ struct UserCharts: View { @State var charts: [userChart] = [] @State var healthData: [HealthData] = [] var body: some View { - VStack{ - Text("Step Count").font(.title) - Chart { - let average = getAverage(healthData: self.healthData) - ForEach(self.healthData) { data in - if let stepCount = data.steps { - BarMark(x: .value("Date", data.date), y: .value("Steps", stepCount)) - RuleMark(y: .value("Average", average)) - .foregroundStyle(Color.secondary) - .lineStyle(StrokeStyle(lineWidth: 0.8, dash: [10])) - .annotation(alignment: .bottomTrailing) { - Text(String(format: "Your average is: %.0f", average)) - .font(.subheadline).bold() - .padding(.trailing, 32) + NavigationStack { + List{ + VStack { + Text("Step Count").font(.title) + Chart { + let averageSteps = getAverage(healthData: healthData) { healthData in + if let steps = healthData.steps { + return Double(steps) + } else { + return nil // Explicitly return nil if there's no value + } + } + ForEach(self.healthData) { data in + if let stepCount = data.steps { + BarMark(x: .value("Date", data.date), y: .value("Steps", stepCount)) + RuleMark(y: .value("Average", averageSteps)) .foregroundStyle(Color.secondary) + .lineStyle(StrokeStyle(lineWidth: 0.8, dash: [10])) + .annotation(alignment: .bottomTrailing) { + Text(String(format: "Your average is: %.0f", averageSteps)) + .font(.subheadline).bold() + .padding(.trailing, 32) + .foregroundStyle(Color.secondary) + } } - } + } + }.frame(height: 150) } - }.frame(height: 150) - Text("Active Energy").font(.title) - Chart { - ForEach(self.healthData) { data in - if let activeEnergy = data.activeEnergy { - BarMark(x: .value("Date", data.date), y: .value("Active Energy", activeEnergy)) - RuleMark(y: .value("Average", 10.0)) - .foregroundStyle(Color.secondary) - .lineStyle(StrokeStyle(lineWidth: 0.8, dash: [10])) - .annotation(alignment: .bottomTrailing) { - Text(String(format: "Your average is: %.0f", 10.0)) - .font(.subheadline).bold() - .padding(.trailing, 32) + VStack { + Text("Active Energy").font(.title) + Chart { + let averageEnergy = getAverage(healthData: healthData) { healthData in + if let val = healthData.activeEnergy { + return Double(val) + } else { + return nil // Explicitly return nil if there's no value + } + } + ForEach(self.healthData) { data in + if let activeEnergy = data.activeEnergy { + BarMark(x: .value("Date", data.date), y: .value("Active Energy", activeEnergy)) + RuleMark(y: .value("Average", averageEnergy)) .foregroundStyle(Color.secondary) + .lineStyle(StrokeStyle(lineWidth: 0.8, dash: [10])) + .annotation(alignment: .bottomTrailing) { + Text(String(format: "Your average is: %.0f", averageEnergy)) + .font(.subheadline).bold() + .padding(.trailing, 32) + .foregroundStyle(Color.secondary) + } } - } + } + }.frame(height: 250) } - }.frame(height: 250) - }.onAppear { - let healthDataFetcher = HealthDataFetcher() - Task { - self.healthData = try await healthDataFetcher.fetchAndProcessHealthData() - } + VStack { + Text("Sleep").font(.title) + Chart { + let average = getAverage(healthData: healthData) { healthData in + if let val = healthData.sleepHours { + return Double(val) + } else { + return nil // Explicitly return nil if there's no value + } + } + ForEach(self.healthData) { data in + if let val = data.sleepHours { + BarMark(x: .value("Date", data.date), y: .value("Sleep", val)) + RuleMark(y: .value("Average", average)) + .foregroundStyle(Color.secondary) + .lineStyle(StrokeStyle(lineWidth: 0.8, dash: [10])) + .annotation(alignment: .bottomTrailing) { + Text(String(format: "Your average is: %.0f", average)) + .font(.subheadline).bold() + .padding(.trailing, 32) + .foregroundStyle(Color.secondary) + } + } + } + }.frame(height: 250) + } + VStack { + Text("Time in Daylight").font(.title) + Chart { + let average = getAverage(healthData: healthData) { healthData in + if let val = healthData.minutesInDaylight { + return Double(val) + } else { + return nil // Explicitly return nil if there's no value + } + } + ForEach(self.healthData) { data in + if let val = data.minutesInDaylight { + BarMark(x: .value("Date", data.date), y: .value("Time in Daylight (minutes)", val)) + RuleMark(y: .value("Average", average)) + .foregroundStyle(Color.secondary) + .lineStyle(StrokeStyle(lineWidth: 0.8, dash: [10])) + .annotation(alignment: .bottomTrailing) { + Text(String(format: "Your average is: %.0f", average)) + .font(.subheadline).bold() + .padding(.trailing, 32) + .foregroundStyle(Color.secondary) + } + } + } + }.frame(height: 250) + } + }.onAppear { + let healthDataFetcher = HealthDataFetcher() + Task { + self.healthData = try await healthDataFetcher.fetchAndProcessHealthData() + } + }.listRowSpacing(10) + .navigationTitle("Charts") } } -- cgit v1.2.3