From 448bad5c96c25b89f4f9db8244fa1fd4e4dcbd8e Mon Sep 17 00:00:00 2001 From: oliverhnat Date: Wed, 27 Nov 2024 14:37:30 +0100 Subject: [PATCH] feat(healthkit): get stepLength and calculate how many steps it takes to get somewhere --- StepMap.xcodeproj/project.pbxproj | 4 ++ StepMap/ContentView.swift | 22 +++++--- StepMap/Managers/HealthKitManager.swift | 53 ++++++++++++++++++++ StepMap/{ => Managers}/LocationManager.swift | 0 StepMap/SearchItemView.swift | 28 ++++++++--- StepMap/SearchView.swift | 22 ++++---- StepMap/StepMap.entitlements | 8 +++ 7 files changed, 114 insertions(+), 23 deletions(-) create mode 100644 StepMap/Managers/HealthKitManager.swift rename StepMap/{ => Managers}/LocationManager.swift (100%) create mode 100644 StepMap/StepMap.entitlements diff --git a/StepMap.xcodeproj/project.pbxproj b/StepMap.xcodeproj/project.pbxproj index fc5a1ac..7774846 100644 --- a/StepMap.xcodeproj/project.pbxproj +++ b/StepMap.xcodeproj/project.pbxproj @@ -249,12 +249,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = StepMap/StepMap.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"StepMap/Preview Content\""; DEVELOPMENT_TEAM = SSJBLTMP95; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHealthShareUsageDescription = "We need to access your step length and walking speed to calculate metrics tailored to you"; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "We need to access your location to set starting point of your route"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "We need to access your location to set starting point of your route"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -280,12 +282,14 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = StepMap/StepMap.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"StepMap/Preview Content\""; DEVELOPMENT_TEAM = SSJBLTMP95; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHealthShareUsageDescription = "We need to access your step length and walking speed to calculate metrics tailored to you"; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "We need to access your location to set starting point of your route"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "We need to access your location to set starting point of your route"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; diff --git a/StepMap/ContentView.swift b/StepMap/ContentView.swift index 9710d94..12d7c1f 100644 --- a/StepMap/ContentView.swift +++ b/StepMap/ContentView.swift @@ -6,20 +6,25 @@ // import CoreLocation +import HealthKitUI import MapKit import SwiftUI struct ContentView: View { @ObservedObject var viewModel = ViewModel() @StateObject var locationManager = LocationManager() + @StateObject var healthKitManager = HealthKitManager() @State private var position = MapCameraPosition.automatic @State private var showSearch: Bool = true @State private var directions: [MKRoute] = [] + @State var healthKitAccess = false + @State var stepLength: Double? // TODO: create a map // Add navigation to the map // after you click the navigation button, show the start and end place on the map with a tag or whatever it's called + // FIX: calling the directions too many times, wait till user is finished typing // add "cancel" button that will hide the route // add ability to hold on the map to place a mark (end goal) // Display the calculated distance and how long will it take by walking @@ -38,11 +43,15 @@ struct ContentView: View { .sheet( isPresented: $showSearch, content: { - SearchView(directions: $directions, locationManager: locationManager) - .ignoresSafeArea() + SearchView( + directions: $directions, stepLength: $stepLength, + locationManager: locationManager + ) + .ignoresSafeArea() } ) .mapControls { + // TODO: make sure the user location stays on the map even if camera moves MapUserLocationButton() .onTapGesture { locationManager.requestAuthorization() @@ -59,12 +68,11 @@ struct ContentView: View { if let userLocation = locationManager.location { position = .camera(MapCamera(centerCoordinate: userLocation, distance: 1000)) } - + Task { + await healthKitManager.requestAccess() + stepLength = await healthKitManager.getStepLength() + } } - // Text("This is what's set: \(viewModel.test)") - // Button(action: { - // save(value: "te5t3") - // }, label: {Text("CLICK ME")}) } func save(value: String) { diff --git a/StepMap/Managers/HealthKitManager.swift b/StepMap/Managers/HealthKitManager.swift new file mode 100644 index 0000000..95bf41e --- /dev/null +++ b/StepMap/Managers/HealthKitManager.swift @@ -0,0 +1,53 @@ +// +// HealthKitManager.swift +// StepMap +// +// Created by Oliver Hnát on 27.11.2024. +// + +import Foundation +import HealthKit + +class HealthKitManager: ObservableObject { + let healthStore = HKHealthStore() + let allTypes: Set = [ + HKQuantityType(.walkingSpeed), + HKQuantityType(.walkingStepLength), + ] + + var stepLength: Double? + + func requestAccess() async { + do { + if HKHealthStore.isHealthDataAvailable() { + try await healthStore.requestAuthorization(toShare: Set(), read: allTypes) + } + } catch { + fatalError( + "Something went wrong while requesting healthKit permissions: \(error.localizedDescription)" + ) + } + } + + func getStepLength() async -> Double? { + if stepLength != nil { + return stepLength + } + let stepLengthType = HKQuantityType(.walkingStepLength) + + let query = HKStatisticsQueryDescriptor( + predicate: HKSamplePredicate.quantitySample(type: stepLengthType), + options: .discreteAverage) + // maybe make an average of all the ones gotten in the last week? + do { + let results = try await query.result(for: healthStore) + stepLength = results?.averageQuantity()?.doubleValue(for: HKUnit.meter()) + return stepLength + } catch { + fatalError( + "Something went wrong while getting step length from healthKit: \(error.localizedDescription)" + ) + } + } + +} diff --git a/StepMap/LocationManager.swift b/StepMap/Managers/LocationManager.swift similarity index 100% rename from StepMap/LocationManager.swift rename to StepMap/Managers/LocationManager.swift diff --git a/StepMap/SearchItemView.swift b/StepMap/SearchItemView.swift index 00807a6..fb4735f 100644 --- a/StepMap/SearchItemView.swift +++ b/StepMap/SearchItemView.swift @@ -5,6 +5,8 @@ // Created by Oliver Hnát on 23.11.2024. // +import HealthKit +import HealthKitUI import MapKit import SwiftUI @@ -12,6 +14,8 @@ struct SearchItemView: View { var location: MKMapItem @State var distance: CLLocationDistance? @Binding var directions: [MKRoute] + @Binding var stepLength: Double? + @Binding var showSteps: Bool @State var localDirections: [MKRoute] = [] var body: some View { @@ -54,7 +58,11 @@ struct SearchItemView: View { } } if distance != nil { - Text("\(formatDistance(distance: distance!))") + Button { + self.showSteps.toggle() + } label: { + Text("\(formatDistance(distance: distance!))") + } } } Spacer() @@ -68,6 +76,15 @@ struct SearchItemView: View { } func formatDistance(distance: CLLocationDistance) -> String { + let steps = distance * (stepLength ?? 0) + if steps != 0 && showSteps { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 0 + formatter.numberStyle = .decimal + let number = NSNumber(value: steps) + return formatter.string(from: number)! + " steps" + // return String(format: "%.0f", steps) + } let distanceFormatter = MKDistanceFormatter() return distanceFormatter.string(fromDistance: distance) } @@ -75,9 +92,9 @@ struct SearchItemView: View { func findDirections() { let directionsRequest = MKDirections.Request() directionsRequest.source = MKMapItem.forCurrentLocation() - // directionsRequest.source = MKMapItem.init( - // placemark: MKPlacemark( - // coordinate: CLLocationCoordinate2D(latitude: 52.3676, longitude: 4.9041))) + // directionsRequest.source = MKMapItem.init( + // placemark: MKPlacemark( + // coordinate: CLLocationCoordinate2D(latitude: 52.3676, longitude: 4.9041))) directionsRequest.destination = location directionsRequest.transportType = .walking directionsRequest.requestsAlternateRoutes = false // TODO: make alternative routes available @@ -86,8 +103,7 @@ struct SearchItemView: View { let searchDirections = MKDirections(request: directionsRequest) searchDirections.calculate { (response, error) in guard let response = response else { - print(error ?? "") - print("Error while searching for directions") + print("Error while searching for directions: \(error?.localizedDescription ?? "")") return } self.localDirections = response.routes diff --git a/StepMap/SearchView.swift b/StepMap/SearchView.swift index 5eadeb7..76e0edd 100644 --- a/StepMap/SearchView.swift +++ b/StepMap/SearchView.swift @@ -9,9 +9,11 @@ import MapKit import SwiftUI struct SearchView: View { - @State private var query: String = "airport" + @State private var query: String = "" @State private var locations: [MKMapItem] = [] @Binding var directions: [MKRoute] + @Binding var stepLength: Double? + @State var showSteps = true var locationManager: LocationManager var body: some View { @@ -27,12 +29,12 @@ struct SearchView: View { self.locations = [] } } - .onAppear { - // TODO: delete this, it's for debug only - search(for: self.query) - } + // .onAppear { + // // TODO: delete this, it's for debug only + // search(for: self.query) + // } .overlay { - HStack{ + HStack { Spacer() Image(systemName: "multiply.circle.fill") .foregroundStyle(.gray) @@ -46,7 +48,9 @@ struct SearchView: View { Spacer() ScrollView { ForEach(self.locations, id: \.identifier) { location in - SearchItemView(location: location, directions: $directions) + SearchItemView( + location: location, directions: $directions, stepLength: $stepLength, + showSteps: $showSteps) } } } @@ -65,7 +69,7 @@ struct SearchView: View { let search = MKLocalSearch(request: searchRequest) search.start { (response, error) in guard let response = response else { - print("ERROR") + print(error) return } var items: [MKMapItem] = [] @@ -73,8 +77,6 @@ struct SearchView: View { if let name = item.name, let location = item.placemark.location { - print( - "\(name): \(location.coordinate.latitude),\(location.coordinate.longitude)") items.append(item) } } diff --git a/StepMap/StepMap.entitlements b/StepMap/StepMap.entitlements new file mode 100644 index 0000000..e10f430 --- /dev/null +++ b/StepMap/StepMap.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.developer.healthkit + + +