Search code examples
iosswiftapple-watchhealthkit

Best approach for checking whether HealthKit permissions have been granted on Apple Watch?


I know that HealthKit still requires users to authorize HealthKit permissions via the iPhone even for a Watch app. In fact, Apple's own Sample Code has no authorization code on the watch whatsoever. If you open Speedy Sloth on the Watch without opening the iPhone (which asks for access) the app will not begin a workout (since it's not authorized) with no notification.

That being said, we watch developers have to implement our own authorization check on the Watch as I understand it and pop an alert to alert the user to open the iPhone app and to authorize when not authorized.

The code below is my implementation of this, however users occasionally report having authorization problems, for example when starting multiple workouts in a row. Can anyone see any flaws in my logic?

import WatchKit
import Foundation
import HealthKit


class InterfaceController: WKInterfaceController {

    // MARK: - Properties

    private let healthStore = HKHealthStore()
    private var healthKitAuthorized = false

    // MARK: - Interface Controller Overrides

    override func awake(withContext context: Any?) {
        super.awake(withContext: context)

    }

    override func willActivate() {
        super.willActivate()

        requestAccessToHealthKit()

    }


    @IBAction func didTapStartButton() {
     segueToWorkOutInterfaceControllerIfAuthorized()
    }

    private func requestAccessToHealthKit() {
        let healthStore = HKHealthStore()

        let healthKitTypesToWrite: Set<HKSampleType> = [
            HKObjectType.workoutType(),
            HKSeriesType.workoutRoute(),
            HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
            HKObjectType.quantityType(forIdentifier: .heartRate)!,
            HKObjectType.quantityType(forIdentifier: .restingHeartRate)!,
            HKObjectType.quantityType(forIdentifier: .bodyMass)!,
            HKObjectType.quantityType(forIdentifier: .vo2Max)!,
            HKObjectType.quantityType(forIdentifier: .stepCount)!,
            HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)!]

        let healthKitTypesToRead: Set<HKObjectType> = [
            HKObjectType.workoutType(),
            HKSeriesType.workoutRoute(),
            HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
            HKObjectType.quantityType(forIdentifier: .heartRate)!,
            HKObjectType.quantityType(forIdentifier: .restingHeartRate)!,
            HKObjectType.characteristicType(forIdentifier: .dateOfBirth)!,
            HKObjectType.quantityType(forIdentifier: .bodyMass)!,
            HKObjectType.quantityType(forIdentifier: .vo2Max)!,
            HKObjectType.quantityType(forIdentifier: .stepCount)!,
            HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)!]


        healthStore.requestAuthorization(toShare: healthKitTypesToWrite, read: healthKitTypesToRead) { (success, error) in

            if let unwrappedError = error {
                DispatchQueue.main.async {

                    print("Watch Healthkit Authorization Error = \(unwrappedError.localizedDescription)")
                    WKInterfaceDevice.current().play(.notification)
                    self.showAlertWith(title: "HealthKit Permission Needed", message: "Open the App on your iPhone or go to -> Settings -> Privacy -> Health -> App and turn on permissions")
                }
            }

            if !success {
                DispatchQueue.main.async {
                    print("Watch Healthkit Authorization was unsuccessful no error reported")
                    WKInterfaceDevice.current().play(.notification)
                    self.showAlertWith(title: "HealthKit Permission Needed", message: "Open the App on your iPhone or go to -> Settings -> Privacy -> Health -> App and turn on permissions")
                }
            }
            DispatchQueue.main.async {
                self.healthKitAuthorized = true
                print("Successful HealthKit Authorization FROM INTERFACE CONTROLLER")
            }
        }
    }
    func showAlertWith(title: String, message: String){

        let action1 = WKAlertAction(title: "OK", style: .default) {
            WKInterfaceController.reloadRootPageControllers(withNames: ["InterfaceController"],
                                                            contexts: nil,
                                                            orientation: .vertical,
                                                            pageIndex: 0)

        }
        presentAlert(withTitle: title, message: message, preferredStyle: .alert, actions: [action1])
    }


    func segueToWorkOutInterfaceControllerIfAuthorized() {
        if healthKitAuthorized {


            WKInterfaceController.reloadRootPageControllers(withNames: ["WorkoutInterfaceController"],
                                                            contexts: [contextDictionary],
                                                            orientation: .vertical,
                                                            pageIndex: 0)
        } else {
            self.showAlertWith(title: "HealthKit Permission Needed", message: "Open your iPhone -> Settings -> Privacy -> Health -> App  and turn on permissions")
        }
    }

}

Solution

  • I decided to put my HealthKit authorization flow into my App's ExtensionDelegate as opposed to an InterfaceController and in testing so far it appears to work/trigger more reliably, and obviously is more efficient that my original code posted given that I do not request authorization unless it is needed. Would still appreciate any input from others on this. 👍

    import WatchKit
    import HealthKit
    import WatchConnectivity
    
    
    class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate {
    
        let healthStore = HKHealthStore()
        var watchSession: WCSession?
        let defaults = UserDefaults.standard
    
        func applicationDidFinishLaunching() {
    
            requestAccessToHealthKit()
    
        }
    
       private func requestAccessToHealthKit() {
    
    
            let healthKitTypesToWrite: Set<HKSampleType> = [
                HKObjectType.workoutType(),
                HKSeriesType.workoutRoute(),
                HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
                HKObjectType.quantityType(forIdentifier: .heartRate)!,
                HKObjectType.quantityType(forIdentifier: .restingHeartRate)!,
                HKObjectType.quantityType(forIdentifier: .bodyMass)!,
                HKObjectType.quantityType(forIdentifier: .vo2Max)!,
                HKObjectType.quantityType(forIdentifier: .stepCount)!,
                HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)!]
    
            let healthKitTypesToRead: Set<HKObjectType> = [
                HKObjectType.workoutType(),
                HKSeriesType.workoutRoute(),
                HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
                HKObjectType.quantityType(forIdentifier: .heartRate)!,
                HKObjectType.quantityType(forIdentifier: .restingHeartRate)!,
                HKObjectType.characteristicType(forIdentifier: .dateOfBirth)!,
                HKObjectType.quantityType(forIdentifier: .bodyMass)!,
                HKObjectType.quantityType(forIdentifier: .vo2Max)!,
                HKObjectType.quantityType(forIdentifier: .stepCount)!,
                HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)!]
    
            let authorizationStatus = healthStore.authorizationStatus(for: HKSampleType.workoutType())
    
            switch authorizationStatus {
    
                case .sharingAuthorized: print("sharing authorized")
                    print("sharing authorized this message is from Watch's extension delegate")
    
                case .sharingDenied: print("sharing denied")
    
                   healthStore.requestAuthorization(toShare: healthKitTypesToWrite, read: healthKitTypesToRead) { (success, error) in
                        print("Successful HealthKit Authorization from Watch's extension Delegate")
                }
    
                default: print("not determined")
    
                healthStore.requestAuthorization(toShare: healthKitTypesToWrite, read: healthKitTypesToRead) { (success, error) in
                    print("Successful HealthKit Authorization from Watch's extension Delegate")
                }
    
            }
        }
    }
    

    Then inside InterfaceController

     @IBAction func didTapStartButton() {
    
            let authorizationStatus = healthStore.authorizationStatus(for: HKSampleType.workoutType())
    
            switch authorizationStatus {
            case .sharingAuthorized: print("sharing authorized")
                 segueToWorkoutInterfaceControllerWithContext()
    
            case .sharingDenied: print("sharing denied")
                self.showAlertWith(title: "HealthKit Permission Denied", message: "Please open The App on your iPhone or go to -> Settings -> Privacy -> Health -> App and turn on all permissions")
    
            default: print("not determined")
                self.showAlertWith(title: "HealthKit Permission Not Determined", message: "Please open The App on your iPhone or go to -> Settings -> Privacy -> Health -> App and turn on all permissions")
            }
    
        }