Hello. I am building an app that shows my walk workouts and in the detail view I want to show the route I took while walking, similar to that of the Apple Fitness App. There is a problem though, I cannot seem to understand how to connect the @State property workoutLocations
array that would be used to draw the route on the map with what I get from the query. The task does successfully fetches the data but then when I try to use it later in a do-catch block nothing happens. What am I missing here?
import SwiftUI
import MapKit
import HealthKit
struct DetailView: View {
@Environment(HealthKitManager.self) var healthKitManager
let workout: HKWorkout
@State private var workoutLocations: [CLLocation] = []
var body: some View {
ScrollView {
//...
}
.task {
guard let store = self.healthKitManager.healthStore else {
fatalError("healthStore is nil. App is in invalid state.")
}
let walkingObjectQuery = HKQuery.predicateForObjects(from: workout)
let routeQuery = HKAnchoredObjectQueryDescriptor(predicates: [.workoutRoute(walkingObjectQuery)], anchor: nil)
let queryResults = routeQuery.results(for: store)
let task = Task {
var workoutRouteLocations: [CLLocation] = []
for try await result in queryResults {
let routeSamples = result.addedSamples
for routeSample in routeSamples {
let routeQueryDescriptor = HKWorkoutRouteQueryDescriptor(routeSample)
let locations = routeQueryDescriptor.results(for: store)
for try await location in locations {
workoutRouteLocations.append(location)
print(workoutRouteLocations.count) // this prints out the number of locations in the sample.
}
}
}
return workoutRouteLocations
}
do {
print(try await task.value.count) // this prints nothing. Therefore if I try to update workoutLocations array from here it would do nothing as well
// workoutLocations = try await task.value therefore does nothing and the array just doesn't get populated with the results of the task
} catch {
print(error)
}
}
}
}
I don't think your code will ever exit the for
loop in the following code snippet:
let routeQuery = HKAnchoredObjectQueryDescriptor(predicates: [.workoutRoute(walkingObjectQuery)], anchor: nil)
let queryResults = routeQuery.results(for: store)
let task = Task {
var workoutRouteLocations: [CLLocation] = []
for try await result in queryResults {
...
}
The reason is that HKAnchoredObjectQueryDescriptor,results(for:) "initiates a long-running query that returns its results using an asynchronous sequence." The for
loop will first deliver the existing data to your code, and then stick there to monitor the changes on the store.
If you need to update the UI while monitoring, do it at the end of the for
loop:
for try await result in queryResults {
let routeSamples = result.addedSamples
for routeSample in routeSamples {
....
}
self.workoutLocations = workoutRouteLocations // Update the state here.
}
If your intent is really just to read the existing data and return, use result(for:) instead, as shown below:
let task = Task {
let store = WorkoutManager.shared.healthStore
let predicate = HKSamplePredicate.workoutRoute(HKQuery.predicateForObjects(from: workout))
var result: HKAnchoredObjectQueryDescriptor<HKWorkoutRoute>.Result
var anchor: HKQueryAnchor? = nil
var workoutRouteLocations: [CLLocation] = []
repeat {
let anchorDescriptor = HKAnchoredObjectQueryDescriptor(predicates: [predicate], anchor: anchor, limit: 100)
result = try await anchorDescriptor.result(for: store)
anchor = result.newAnchor
for routeSample in result.addedSamples {
let routeQueryDescriptor = HKWorkoutRouteQueryDescriptor(routeSample)
let locations = routeQueryDescriptor.results(for: store)
for try await location in locations {
workoutRouteLocations.append(location)
print(workoutRouteLocations.count) // this prints out the number of locations in the sample.
}
}
} while (result.addedSamples != []) && (result.deletedObjects != [])
return workoutRouteLocations
}
do {
let locationCount = try await task.value.count
print ("Total locations after task finished: \(locationCount)")
} catch let error {
print(error)
}
Best,
——
Ziqiao Chen
Worldwide Developer Relations.