From b8befff442ec910b2c6343f02699a66319667afb Mon Sep 17 00:00:00 2001 From: Navan Chauhan Date: Sat, 10 Feb 2024 22:18:43 -0700 Subject: add workout card --- iCUrHealth/ContentView.swift | 246 +++++++++++++++++++++++++++++----- iCUrHealth/DataAtAGlance.swift | 1 + iCUrHealth/DetailedAnalysisView.swift | 3 + iCUrHealth/HealthData.swift | 8 +- 4 files changed, 221 insertions(+), 37 deletions(-) diff --git a/iCUrHealth/ContentView.swift b/iCUrHealth/ContentView.swift index 812dd56..78cf965 100644 --- a/iCUrHealth/ContentView.swift +++ b/iCUrHealth/ContentView.swift @@ -9,6 +9,7 @@ import SwiftUI import HealthKit import HealthKitUI import Charts +import MapKit struct chartData: Identifiable { let tag: String @@ -19,6 +20,7 @@ struct chartData: Identifiable { let allTypes: Set = [ HKQuantityType.workoutType(), + HKSeriesType.workoutRoute(), HKQuantityType(.activeEnergyBurned), HKQuantityType(.distanceCycling), HKQuantityType(.distanceWalkingRunning), @@ -28,56 +30,212 @@ let allTypes: Set = [ HKCategoryType(.sleepAnalysis) ] +import HealthKit +import CoreLocation + +struct WorkoutRoute { + var coordinates: [CLLocationCoordinate2D] +} + +class WorkoutViewModel: ObservableObject { + @Published var workout: HKWorkout? + @Published var workoutRoute: WorkoutRoute? { + didSet { + workoutRouteCoordinates = workoutRoute?.coordinates ?? [] + } + } + @Published var workoutRouteCoordinates: [CLLocationCoordinate2D] = [] + private var healthStore = HKHealthStore() + + func fetchAndProcessWorkoutRoute() { + let workoutPredicate = HKQuery.predicateForWorkouts(with: .downhillSkiing) + let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) + + let workoutQuery = HKSampleQuery(sampleType: .workoutType(), predicate: workoutPredicate, limit: 1, sortDescriptors: [sortDescriptor]) { (query, samples, error) in + guard let workouts = samples as? [HKWorkout], error == nil else { + // Handle the error here. + return + } + + // Process the fetched workouts here. + for workout in workouts { + let routePredicate = HKQuery.predicateForObjects(from: workout) + let routeQuery = HKAnchoredObjectQuery(type: HKSeriesType.workoutRoute(), predicate: routePredicate, anchor: nil, limit: HKObjectQueryNoLimit) { (query, routeSamples, deletedObjects, anchor, error) in + guard let routeSamples = routeSamples as? [HKWorkoutRoute], error == nil else { + // Handle the error here + return + } + + for routeSample in routeSamples { + let workoutRouteQuery = HKWorkoutRouteQuery(route: routeSample) { (query, locationsOrNil, done, errorOrNil) in + guard let locations = locationsOrNil, errorOrNil == nil else { + // Handle error + return + } + + let allCoordinates = locations.map { $0.coordinate } + + // Once all coordinates are fetched, update the published property + DispatchQueue.main.async { + self.workoutRoute = WorkoutRoute(coordinates: allCoordinates) + self.workout = workout + } + + if done { + // Finish processing as needed + } + } + self.healthStore.execute(workoutRouteQuery) + } + } + self.healthStore.execute(routeQuery) + } + } + + self.healthStore.execute(workoutQuery) + } +} + +struct IdentifiableCoordinate: Identifiable { + let id = UUID() + var coordinate: CLLocationCoordinate2D +} + +struct MapView: View { + var route: WorkoutRoute + + // Convert coordinates to identifiable coordinates + var identifiableCoordinates: [IdentifiableCoordinate] { + route.coordinates.map { IdentifiableCoordinate(coordinate: $0) } + } + + var body: some View { + Map(coordinateRegion: .constant(regionForRoute()), + showsUserLocation: false, + userTrackingMode: .none, + annotationItems: identifiableCoordinates) { item in + MapPin(coordinate: item.coordinate, tint: .blue) + } + .overlay( + MapOverlay(coordinates: route.coordinates) + .stroke(Color.blue, lineWidth: 3) + ) + .cornerRadius(10) // Optional: Adds rounded corners to the map + } + + func regionForRoute() -> MKCoordinateRegion { +// guard let firstCoordinate = route.coordinates.first else { +// return MKCoordinateRegion() +// } + + let count = route.coordinates.count / 2 + guard let firstCoordinate = route.coordinates.prefix(count).last else { + return MKCoordinateRegion() + } + + return MKCoordinateRegion(center: firstCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)) + } +} + +struct MapOverlay: Shape { + var coordinates: [CLLocationCoordinate2D] + + func path(in rect: CGRect) -> Path { + var path = Path() + + guard let firstCoordinate = coordinates.first else { + return path + } + + let mapRect = MKMapRect.world + let firstPoint = MKMapPoint(firstCoordinate) + let startPoint = CGPoint(x: (firstPoint.x / mapRect.size.width) * rect.size.width, y: (1 - firstPoint.y / mapRect.size.height) * rect.size.height) + path.move(to: startPoint) + + for coordinate in coordinates.dropFirst() { + let mapPoint = MKMapPoint(coordinate) + let point = CGPoint(x: (mapPoint.x / mapRect.size.width) * rect.size.width, y: (1 - mapPoint.y / mapRect.size.height) * rect.size.height) + path.addLine(to: point) + } + + return path + } +} + + struct ContentView: View { @State var authenticated = false @State var trigger = false + @StateObject private var viewModel = WorkoutViewModel() + let healthStore = HKHealthStore() @State private var data: [chartData] = [] var body: some View { - NavigationView { TabView{ - VStack { - HStack { - Text("Steps") - Chart(data) { - BarMark(x: .value("Date", $0.dateInterval), - y: .value("Count", $0.data) - ) - - } - }.frame(maxHeight: 100) - Button(action: { - Task { - try await fetchStepCountData() - } - }) - { - Text("Exp Function") - } - Spacer() - .onAppear() { - if HKHealthStore.isHealthDataAvailable() { - trigger.toggle() + NavigationView { + List { + HStack { + Text("Steps") + Chart(data) { + BarMark(x: .value("Date", $0.dateInterval), + y: .value("Count", $0.data) + ) + + } + }.frame(maxHeight: 100) + .navigationTitle("iCUrHealth") + Button(action: { + Task { + try await fetchStepCountData() } + }) + { + Text("Exp Function") } - .healthDataAccessRequest(store: healthStore, - readTypes: allTypes, - trigger: trigger) { result in - switch result { - - case .success(_): - authenticated = true - Task { - try await fetchStepCountData() + 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!)") + } } - case .failure(let error): - // Handle the error here. - fatalError("*** An error occurred while requesting authentication: \(error) ***") } + } else { + Text("Fetching workout route...") + .onAppear { + if HKHealthStore.isHealthDataAvailable() { + trigger.toggle() + } + viewModel.fetchAndProcessWorkoutRoute() + } + .healthDataAccessRequest(store: healthStore, + readTypes: allTypes, + trigger: trigger) { result in + switch result { + + case .success(_): + authenticated = true + Task { + try await fetchStepCountData() + } + case .failure(let error): + // Handle the error here. + fatalError("*** An error occurred while requesting authentication: \(error) ***") + } + } } + + }.listRowSpacing(10) } .padding() .navigationTitle("iCUrHealth") @@ -97,6 +255,24 @@ struct ContentView: View { Text("Trends") } } + + } + + private func experimentWithSkiWorkout() async throws { + let workoutPredicate = HKQuery.predicateForWorkouts(with: .downhillSkiing) + let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) + + let workoutQuery = HKSampleQuery(sampleType: .workoutType(), predicate: workoutPredicate, limit: 3, sortDescriptors: [sortDescriptor]) { (query, samples, error) in + + guard let workouts = samples as? [HKWorkout], error == nil else { + // Handle the error here. + return + } + + // Process the fetched workouts here. + for workout in workouts { + print(workout) + } } } diff --git a/iCUrHealth/DataAtAGlance.swift b/iCUrHealth/DataAtAGlance.swift index aef3315..b610bf9 100644 --- a/iCUrHealth/DataAtAGlance.swift +++ b/iCUrHealth/DataAtAGlance.swift @@ -156,6 +156,7 @@ struct DataAtAGlance: View { } else { initial_count += 1 } + print(myValue.date) } init_avg = initial_total / initial_count diff --git a/iCUrHealth/DetailedAnalysisView.swift b/iCUrHealth/DetailedAnalysisView.swift index 1ca9e6f..e13969c 100644 --- a/iCUrHealth/DetailedAnalysisView.swift +++ b/iCUrHealth/DetailedAnalysisView.swift @@ -7,12 +7,15 @@ import SwiftUI import Charts +import Foundation struct DetailedAnalysisView: View { @State var healthData: [HealthData] @State var llmInput: String = "" let prediction: Analysis + let dateFormatter = DateFormatter() + var body: some View { NavigationView { diff --git a/iCUrHealth/HealthData.swift b/iCUrHealth/HealthData.swift index 38f95ed..1eebec2 100644 --- a/iCUrHealth/HealthData.swift +++ b/iCUrHealth/HealthData.swift @@ -26,7 +26,7 @@ extension Date { struct HealthData: Codable, Identifiable { var id = UUID() - var date: String + var date: Date var steps: Double? var activeEnergy: Double? var exerciseMinutes: Double? @@ -195,12 +195,16 @@ extension HealthDataFetcher { let calendar = Calendar.current let today = Date() var healthData: [HealthData] = [] + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "M/d/yy" + for day in 1...14 { guard let endDate = calendar.date(byAdding: .day, value: -day, to: today) else { continue } healthData.append( HealthData( - date: DateFormatter.localizedString(from: endDate, dateStyle: .short, timeStyle: .none) + date: dateFormatter.date(from: DateFormatter.localizedString(from: endDate, dateStyle: .short, timeStyle: .none))! ) ) } -- cgit v1.2.3