swiftswiftuihealthkit

HealthKit fetching route fails to - Authorization not determined


Trying and failing to get route/location data from a workout due to denied authorization. I'm only asking for 'read' values of HKObjectType.workoutType. I have all plist keys added, I have capability setup. Running on a physical device with data.

Error: Error Domain=com.apple.healthkit Code=5 "Authorization not determined" UserInfo={NSLocalizedDescription=Authorization not determined}

Not sure why. I even tried adding 'sharing' request but still hit the error. The 'permissions UI' appears and I can view a list of workouts, it just fails unexpectedly (for me) when loading a detail view.

Based on: https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings/reading_route_data

ContentView

struct ContentView: View {
    @State private var isPresented = false
    private let healthStore = HKHealthStore()

    var body: some View {
        NavigationStack {
            Button("Request") {
                requestAuthorization()
            }
            .navigationDestination(isPresented: $isPresented) {
                WorkoutListView()
            }
        }
    }
        
    private func requestAuthorization() {
        let typesToRead: Set<HKObjectType> = [HKObjectType.workoutType()]

        healthStore.requestAuthorization(toShare: nil, read: typesToRead) { success, error in
            if let error = error {
                print("Authorization request failed: \(error.localizedDescription)")
                return
            }
            
            if success {
                print("Authorization granted")
                isPresented = true
                }
            } else {
                print("Authorization denied")
            }
        }
    }
}

WorkoutListView

struct WorkoutListView: View {
    @StateObject var viewModel = WorkoutListViewModel()
    
    var body: some View {
        List {
            Section("Walk") {
                ForEach(viewModel.workouts, id: \.uuid) { workout in
                    if case .walking = workout.workoutActivityType {
                        NavigationLink(destination: WorkoutDetailView(workout: workout)) {
                            HStack {
                                Image(systemName: "figure.walk")
                                Text("Outdoor Walk")
                                }
                            }
                        }
                    }
                }
            }
        }
        .onAppear {
            viewModel.fetchWorkouts()
        }
        .navigationTitle("Workouts")
    }
}

WorkoutDetailView

struct WorkoutDetailView: View {
    var workout: HKWorkout
    @StateObject var viewModel = WorkoutListViewModel()
    @State private var location: CLLocation?
    
    var body: some View {
        VStack {
            // Display location data if available
            if let location = location {
                Text("Latitude: \(location.coordinate.latitude)")
                Text("Longitude: \(location.coordinate.longitude)")
                Text("Time: \(location.timestamp)")
            } else {
                Text("Location data not available.")
            }
        }
        .onAppear {
            viewModel.getLocationData(for: workout) { extractedLocation in
                location = extractedLocation
            }
        }
    }
}

WorkoutListViewModel

final class WorkoutListViewModel: ObservableObject {
    @Published private(set) var workouts: [HKWorkout] = []
    private let healthStore = HKHealthStore()

    // Fetch today's walking exercises
    func fetchWorkouts() {
        let workoutType = HKObjectType.workoutType()
        
        // Define the start and end dates for today
        let calendar = Calendar.current
        let startDate = calendar.startOfDay(for: Date())
        let endDate = calendar.date(byAdding: .day, value: 1, to: startDate)
        
        // Create a predicate to fetch workouts that fall within the specified date range
        let datePredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
        
        // Create a predicate to filter workouts by activity type (e.g., walking)
        let activityPredicate = HKQuery.predicateForWorkouts(with: .walking)
        
        // Combine the date and activity predicates
        let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [datePredicate, activityPredicate])
        
        let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
        let query = HKSampleQuery(
            sampleType: workoutType,
            predicate: compoundPredicate,
            limit: 0,
            sortDescriptors: [sortDescriptor]
        ) { query, samples, error in
            
            guard let samples = samples as? [HKWorkout], error == nil else {
                print("Failed to fetch workouts: \(error?.localizedDescription ?? "Unknown error")")
                return
            }
            
            DispatchQueue.main.async {
                self.workouts = samples
            }
        }
        
        healthStore.execute(query)
    }

    // Get location data for a workout
    func getLocationData(for workout: HKWorkout, completion: @escaping (CLLocation?) -> Void) {
        // 1. Get Route
        let runningObjectQuery = HKQuery.predicateForObjects(from: workout)
        let routeQuery = HKAnchoredObjectQuery(
            type: HKSeriesType.workoutRoute(),
            predicate: runningObjectQuery,
            anchor: nil,
            limit: HKObjectQueryNoLimit
        ) { (query, samples, deletedObjects, anchor, error) in
            
            guard error == nil else {
                // Handle any errors here.
                print(error!)
                fatalError("The initial query failed.")
            }
            
            // Process the initial route data here.
        }
        
        // 2. Location Data from Route
            
        // Execute
        healthStore.execute(routeQuery)
    }
}

Solution

  • To get the location details of the workout that you're requesting when the WorkoutDetailView appears, you'll also need to request access to HKSeriesType.workoutRoute().

    So your request will be

    let typesToRead: Set<HKObjectType> = [HKObjectType.workoutType(), HKSeriesType.workoutRoute()]