From 92604ea25d5c42de0ded3211d79ba7e8f6ac66d2 Mon Sep 17 00:00:00 2001 From: Navan Chauhan Date: Sat, 10 Feb 2024 22:55:56 -0700 Subject: rebase --- iCUrHealth/ContentView.swift | 270 +--------------------------------------- iCUrHealth/HomeView.swift | 154 +++++++++++++++++++++++ iCUrHealth/UserCharts.swift | 6 +- iCUrHealth/WorkoutMapView.swift | 140 +++++++++++++++++++++ 4 files changed, 299 insertions(+), 271 deletions(-) create mode 100644 iCUrHealth/HomeView.swift create mode 100644 iCUrHealth/WorkoutMapView.swift diff --git a/iCUrHealth/ContentView.swift b/iCUrHealth/ContentView.swift index 78cf965..9a85785 100644 --- a/iCUrHealth/ContentView.swift +++ b/iCUrHealth/ContentView.swift @@ -30,139 +30,6 @@ 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 @@ -175,71 +42,7 @@ struct ContentView: View { var body: some View { TabView{ - 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") - } - 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!)") - } - } - } - } 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") - + HomeView() .tabItem { Image(systemName: "gear") Text("Home") @@ -257,75 +60,4 @@ struct ContentView: View { } } - - 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) - } - } - } - - private func fetchStepCountData() async throws { - let calendar = Calendar(identifier: .gregorian) - let today = calendar.startOfDay(for: Date()) - - - guard let endDate = calendar.date(byAdding: .day, value: 1, to: today) else { - fatalError("*** Unable to calculate the end time ***") - } - - - guard let startDate = calendar.date(byAdding: .day, value: -29, to: endDate) else { - fatalError("*** Unable to calculate the start time ***") - } - - - let thisWeek = HKQuery.predicateForSamples(withStart: startDate, end: endDate) - - - // Create the query descriptor. - let stepType = HKQuantityType(.stepCount) - let stepsThisWeek = HKSamplePredicate.quantitySample(type: stepType, predicate:thisWeek) - let everyDay = DateComponents(day:1) - - - let sumOfStepsQuery = HKStatisticsCollectionQueryDescriptor( - predicate: stepsThisWeek, - options: .cumulativeSum, - anchorDate: endDate, - intervalComponents: everyDay) - - - let stepCounts = try await sumOfStepsQuery.result(for: self.healthStore) - - var dailyData: [chartData] = [] - - stepCounts.enumerateStatistics(from: startDate, to: endDate) { stats, _ in - if let quantity = stats.sumQuantity() { - //print(quantity, stats.startDate) - dailyData.append( - chartData(tag: "activity", dateInterval: stats.startDate, data: quantity.doubleValue(for: HKUnit.count())) - ) - } else { - } - - } - - data = dailyData - } -} - -#Preview { - ContentView() } diff --git a/iCUrHealth/HomeView.swift b/iCUrHealth/HomeView.swift new file mode 100644 index 0000000..c29758a --- /dev/null +++ b/iCUrHealth/HomeView.swift @@ -0,0 +1,154 @@ +// +// HomeView.swift +// iCUrHealth +// +// Created by Navan Chauhan on 2/10/24. +// + +import SwiftUI +import HealthKit +import HealthKitUI +import Charts + +struct HomeView: 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 { + List { + 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") + } + 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!)") + } + } + } + } 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) + .navigationTitle("iCUrHealth") + } + } + + 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) + } + } + } + + private func fetchStepCountData() async throws { + let calendar = Calendar(identifier: .gregorian) + let today = calendar.startOfDay(for: Date()) + + + guard let endDate = calendar.date(byAdding: .day, value: 1, to: today) else { + fatalError("*** Unable to calculate the end time ***") + } + + + guard let startDate = calendar.date(byAdding: .day, value: -29, to: endDate) else { + fatalError("*** Unable to calculate the start time ***") + } + + + let thisWeek = HKQuery.predicateForSamples(withStart: startDate, end: endDate) + + + // Create the query descriptor. + let stepType = HKQuantityType(.stepCount) + let stepsThisWeek = HKSamplePredicate.quantitySample(type: stepType, predicate:thisWeek) + let everyDay = DateComponents(day:1) + + + let sumOfStepsQuery = HKStatisticsCollectionQueryDescriptor( + predicate: stepsThisWeek, + options: .cumulativeSum, + anchorDate: endDate, + intervalComponents: everyDay) + + + let stepCounts = try await sumOfStepsQuery.result(for: self.healthStore) + + var dailyData: [chartData] = [] + + stepCounts.enumerateStatistics(from: startDate, to: endDate) { stats, _ in + if let quantity = stats.sumQuantity() { + //print(quantity, stats.startDate) + dailyData.append( + chartData(tag: "activity", dateInterval: stats.startDate, data: quantity.doubleValue(for: HKUnit.count())) + ) + } else { + } + + } + + data = dailyData + } +} diff --git a/iCUrHealth/UserCharts.swift b/iCUrHealth/UserCharts.swift index c44b84c..0fdf9d5 100644 --- a/iCUrHealth/UserCharts.swift +++ b/iCUrHealth/UserCharts.swift @@ -43,7 +43,9 @@ func getAverage(healthData: [HealthData]) -> Double { } struct UserCharts: View { - @State private var healthData: [HealthData] = [] +// var charts: [userChart] + @State var charts: [userChart] = [] + @State var healthData: [HealthData] = [] var body: some View { VStack{ Text("Step Count").font(.title) @@ -81,7 +83,7 @@ struct UserCharts: View { } } }.frame(height: 250) - }.onAppear{ + }.onAppear { let healthDataFetcher = HealthDataFetcher() Task { self.healthData = try await healthDataFetcher.fetchAndProcessHealthData() diff --git a/iCUrHealth/WorkoutMapView.swift b/iCUrHealth/WorkoutMapView.swift new file mode 100644 index 0000000..48617a2 --- /dev/null +++ b/iCUrHealth/WorkoutMapView.swift @@ -0,0 +1,140 @@ +// +// WorkoutMapView.swift +// iCUrHealth +// +// Created by Navan Chauhan on 2/10/24. +// + +import SwiftUI +import HealthKit +import CoreLocation +import MapKit + +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 + } +} -- cgit v1.2.3