feat(healthkit): get stepLength and calculate how many steps it takes to get somewhere
This commit is contained in:
@@ -249,12 +249,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 = StepMap/StepMap.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"StepMap/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"StepMap/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = SSJBLTMP95;
|
DEVELOPMENT_TEAM = SSJBLTMP95;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = 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_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_NSLocationWhenInUseUsageDescription = "We need to access your location to set starting point of your route";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
@@ -280,12 +282,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 = StepMap/StepMap.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"StepMap/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"StepMap/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = SSJBLTMP95;
|
DEVELOPMENT_TEAM = SSJBLTMP95;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = 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_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_NSLocationWhenInUseUsageDescription = "We need to access your location to set starting point of your route";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
|
|||||||
@@ -6,20 +6,25 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
|
import HealthKitUI
|
||||||
import MapKit
|
import MapKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@ObservedObject var viewModel = ViewModel()
|
@ObservedObject var viewModel = ViewModel()
|
||||||
@StateObject var locationManager = LocationManager()
|
@StateObject var locationManager = LocationManager()
|
||||||
|
@StateObject var healthKitManager = HealthKitManager()
|
||||||
|
|
||||||
@State private var position = MapCameraPosition.automatic
|
@State private var position = MapCameraPosition.automatic
|
||||||
@State private var showSearch: Bool = true
|
@State private var showSearch: Bool = true
|
||||||
@State private var directions: [MKRoute] = []
|
@State private var directions: [MKRoute] = []
|
||||||
|
@State var healthKitAccess = false
|
||||||
|
@State var stepLength: Double?
|
||||||
|
|
||||||
// TODO: create a map
|
// TODO: create a map
|
||||||
// Add navigation to the 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
|
// 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 "cancel" button that will hide the route
|
||||||
// add ability to hold on the map to place a mark (end goal)
|
// 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
|
// Display the calculated distance and how long will it take by walking
|
||||||
@@ -38,11 +43,15 @@ struct ContentView: View {
|
|||||||
.sheet(
|
.sheet(
|
||||||
isPresented: $showSearch,
|
isPresented: $showSearch,
|
||||||
content: {
|
content: {
|
||||||
SearchView(directions: $directions, locationManager: locationManager)
|
SearchView(
|
||||||
|
directions: $directions, stepLength: $stepLength,
|
||||||
|
locationManager: locationManager
|
||||||
|
)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.mapControls {
|
.mapControls {
|
||||||
|
// TODO: make sure the user location stays on the map even if camera moves
|
||||||
MapUserLocationButton()
|
MapUserLocationButton()
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
locationManager.requestAuthorization()
|
locationManager.requestAuthorization()
|
||||||
@@ -59,12 +68,11 @@ struct ContentView: View {
|
|||||||
if let userLocation = locationManager.location {
|
if let userLocation = locationManager.location {
|
||||||
position = .camera(MapCamera(centerCoordinate: userLocation, distance: 1000))
|
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) {
|
func save(value: String) {
|
||||||
|
|||||||
53
StepMap/Managers/HealthKitManager.swift
Normal file
53
StepMap/Managers/HealthKitManager.swift
Normal file
@@ -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)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
// Created by Oliver Hnát on 23.11.2024.
|
// Created by Oliver Hnát on 23.11.2024.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import HealthKit
|
||||||
|
import HealthKitUI
|
||||||
import MapKit
|
import MapKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@@ -12,6 +14,8 @@ struct SearchItemView: View {
|
|||||||
var location: MKMapItem
|
var location: MKMapItem
|
||||||
@State var distance: CLLocationDistance?
|
@State var distance: CLLocationDistance?
|
||||||
@Binding var directions: [MKRoute]
|
@Binding var directions: [MKRoute]
|
||||||
|
@Binding var stepLength: Double?
|
||||||
|
@Binding var showSteps: Bool
|
||||||
@State var localDirections: [MKRoute] = []
|
@State var localDirections: [MKRoute] = []
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -54,9 +58,13 @@ struct SearchItemView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if distance != nil {
|
if distance != nil {
|
||||||
|
Button {
|
||||||
|
self.showSteps.toggle()
|
||||||
|
} label: {
|
||||||
Text("\(formatDistance(distance: distance!))")
|
Text("\(formatDistance(distance: distance!))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,6 +76,15 @@ struct SearchItemView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func formatDistance(distance: CLLocationDistance) -> String {
|
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()
|
let distanceFormatter = MKDistanceFormatter()
|
||||||
return distanceFormatter.string(fromDistance: distance)
|
return distanceFormatter.string(fromDistance: distance)
|
||||||
}
|
}
|
||||||
@@ -86,8 +103,7 @@ struct SearchItemView: View {
|
|||||||
let searchDirections = MKDirections(request: directionsRequest)
|
let searchDirections = MKDirections(request: directionsRequest)
|
||||||
searchDirections.calculate { (response, error) in
|
searchDirections.calculate { (response, error) in
|
||||||
guard let response = response else {
|
guard let response = response else {
|
||||||
print(error ?? "")
|
print("Error while searching for directions: \(error?.localizedDescription ?? "")")
|
||||||
print("Error while searching for directions")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.localDirections = response.routes
|
self.localDirections = response.routes
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import MapKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SearchView: View {
|
struct SearchView: View {
|
||||||
@State private var query: String = "airport"
|
@State private var query: String = ""
|
||||||
@State private var locations: [MKMapItem] = []
|
@State private var locations: [MKMapItem] = []
|
||||||
@Binding var directions: [MKRoute]
|
@Binding var directions: [MKRoute]
|
||||||
|
@Binding var stepLength: Double?
|
||||||
|
@State var showSteps = true
|
||||||
var locationManager: LocationManager
|
var locationManager: LocationManager
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -27,10 +29,10 @@ struct SearchView: View {
|
|||||||
self.locations = []
|
self.locations = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
// .onAppear {
|
||||||
// TODO: delete this, it's for debug only
|
// // TODO: delete this, it's for debug only
|
||||||
search(for: self.query)
|
// search(for: self.query)
|
||||||
}
|
// }
|
||||||
.overlay {
|
.overlay {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -46,7 +48,9 @@ struct SearchView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
ScrollView {
|
ScrollView {
|
||||||
ForEach(self.locations, id: \.identifier) { location in
|
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)
|
let search = MKLocalSearch(request: searchRequest)
|
||||||
search.start { (response, error) in
|
search.start { (response, error) in
|
||||||
guard let response = response else {
|
guard let response = response else {
|
||||||
print("ERROR")
|
print(error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var items: [MKMapItem] = []
|
var items: [MKMapItem] = []
|
||||||
@@ -73,8 +77,6 @@ struct SearchView: View {
|
|||||||
if let name = item.name,
|
if let name = item.name,
|
||||||
let location = item.placemark.location
|
let location = item.placemark.location
|
||||||
{
|
{
|
||||||
print(
|
|
||||||
"\(name): \(location.coordinate.latitude),\(location.coordinate.longitude)")
|
|
||||||
items.append(item)
|
items.append(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
StepMap/StepMap.entitlements
Normal file
8
StepMap/StepMap.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>
|
||||||
Reference in New Issue
Block a user