Problem with connecting the workout data to a SwiftUI View.

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)
            }
        }
    }
}
Answered by DTS Engineer in 822543022

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.

Accepted Answer

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.

@DTS Engineer Thank you for your reply. I now understand the problem I had. I just have another question now. If I want to move all of this code to HealthKitManager class is this the optimal way to do it?

struct WorkoutDetailView: View {
    
    @Environment(HealthKitManager.self) var healthKitManager
    let workout: HKWorkout
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                //...
            }
            .task {
                do {
                    workoutLocations = try await healthKitManager.retrieveWorkoutRoute(for: workout)
                } catch {
                    fatalError("error while fetching workout route: \(error)")
                }
            }
        }
    }
}


@Observable
class HealthKitManger {
private var healthStore: HKHealthStore?

//...

func retrieveWorkoutRoute(for workout: HKWorkout) async throws -> [CLLocation] {
        guard let store = self.healthStore else {
            fatalError("retrieveWorkoutRoute(): healthStore is nil. App is in invalid state.")
        }
        
        
        let currentWorkoutPredicate = HKQuery.predicateForObjects(from: workout)
        
        
        let routeSamplesQuery = HKAnchoredObjectQueryDescriptor(predicates: [.workoutRoute(currentWorkoutPredicate)], anchor: nil)
        
        
        let queryResults = routeSamplesQuery.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)
                    }
                }
                
                return workoutRouteLocations
            }
            
            return workoutRouteLocations
        }
        
        return try await task.value
    }
}
Problem with connecting the workout data to a SwiftUI View.
 
 
Q