Building a Scalable Apple Health Authorization Management View for iOS

Emre Havan
Fit Records
Published in
14 min readJan 9, 2024

It usually seems pretty easy to ask for authorization for Apple Health from our iOS apps, we just need to call the requestAuthorization method, and if the user has not yet authorized or made a decision, they will see the authorization sheet, and if they had, they will see nothing. Well yeah, it’s pretty easy, but building a better experience for users, where they can see, in detail, whether they have authorized, or what data types they have authorized, or if there are new data types the app needs additional permissions for, can be a challenging and complicated task.

In this piece, we are going to build the same experience we have build for Fit Records. Lets get started!

Disclaimer: In some parts of the code, we could have also used the new Swift Concurrency and also Dependency Injection for testability. But these are omitted for this article since it is focused on HealthKit Integration

Getting To Know Data Types and APIs

First, let’s identify the data types we are going to work with and take a look at the HealthKit APIs we are going to use.

In this example, we are going to request authorization for writing and reading data as follows:

Write Data Types: Active Calories Burned, Workouts

Read Data Types: Active Calories Burned, Heart Rate

Now the HealthKit APIs we will use:

  • requestAuthorization(toShare:read:completion:): A method to request authorization for given write and read types, if user has not seen the permission view for the given types, the system presents the permission view, otherwise it calls the completion of the method directly.
  • getRequestStatusForAuthorization(toShare:read:completion:): A method to understand whether the user has seen the permission view for the given write and read types. This will help us to show an authorize additional permissions button when we introduce new types in the future.
  • authorizationStatus(for:): A method to check the authorization status for the given health type. IMPORTANT! It is not possible to figure out whether user has granted permission for reading a certain data type, it only works for checking the status for writing a certain data type.

Now that we’ve reviewed the data types and the important APIs, we can get started with building our view.

Building the View

We are going to build our view using SwiftUI, but before we get started with building the view, it is important to first define what we want to achieve, then build the viewModel (ObservableObject) which will be responsible for managing the view state for different HealthKit authorization states.

What we want to achieve

The transition from not determined to authorized state

We want to provide an informative Apple Health Integration Management View for our users, and it can be best described with its different states.

  • User has not seen the authorization view: In this state, the user has not yet made a decision on authorizing any health data, we want to show them a button to trigger the authorization flow
  • User has denied authorization for all data types: In this state, the user has denied providing access to any health data, we want to show that the Health is not integrated
  • User has denied authorization for all write data types, but authorized for some read data types: Since there is no way for us to understand if the user has granted access for a read type, if the user has denied access for all write types, we will assume the Health is not integrated, and the UI will be the same as the state above
  • User has granted access for all or some write data types: In this state, we want to show users that the integration is active and indicate the authorization status for each write data type
  • User has granted access for all or some write data types, but the app needs additional permission for a new write data type: In this state, we want to show users that the integration is active, indicating the authorization status for determined data types, and show that new data types are available for additional access, and a button to reauthorize the Health.
  • User has granted access for all or some write data types, but the app needs additional permission for a new read data type: The state is the same as above, but this time the app introduced a new read data type, since we cannot know if the user has granted permission for a read type, we only want to show a button to reauthorize the Health, without showing the status for this new read data type

Phew, thats a lot of states, and we want to cover all of them in our UI.

We better get to work!

Implementing the View Model

We are going to implement an observable view model that will manage all the logic around different states. Before we get started with it though, let’s implement an enum to describe all the possible states we described above.

We are actually going to need two enums, one for describing the write data types we are interested in, and the second one to manage the state of the authorization status.

First, AppleHealthWriteDataTypes for describing the write data types we will use:

enum AppleHealthWriteDataTypes: Hashable, Identifiable {
var id: Self {
return self
}

case activeCaloriesBurned(HKAuthorizationStatus)
case workout(HKAuthorizationStatus)
}

The cases also have an associated value of type HKAuthorizationStatus it will be helpful for us to indicate whether a data type is authorized or not, later on when we draw the UI. Also, it conforms to Hashable and Identifiable so we can use it in a ForEach .

Now the main enum to manage all the state, HealthKitIntegrationState :

enum HealthKitIntegrationState {
case healthDataNotAvailable
case loading
case notDetermined
case determined([AppleHealthWriteDataTypes])
case partiallyDetermined(determinedWriteTypes: [AppleHealthWriteDataTypes], nonDeterminedWriteTypes: [AppleHealthWriteDataTypes])
}

Lets go over them one by one:

  • healthDataNotAvailable: HealthKit is not available for older iPads and Macbooks (partially), so depending on the user’s device, the health data may not be available. Learn more at isHealthDataAvailable
  • loading: This is the state we will set initially, until we get more information about user’s apple health state
  • notDetermined: User has not made a decision yet
  • determined: The Apple Health state is determined
  • partiallyDetermined: User previously determined the state for data types, but the app introduced new data types that require additional determination

Some Helpers

We are also going to implement some helper entities to get the read and write types we use, request authorization and check authorization status for given types.

AppleHealthUsedDataTypeProvider

A simple enum with two static functions to provide read and write data types for HealthKit

enum AppleHealthUsedDataTypeProvider {
static func provideReadTypes() -> Set<HKObjectType>? {
guard HKHealthStore.isHealthDataAvailable(),
let activeCaloriesBurned = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
let heartRate = HKObjectType.quantityType(forIdentifier: .heartRate) else {
return nil
}
return [activeCaloriesBurned, heartRate]
}

static func provideWriteTypes() -> Set<HKSampleType>? {
guard HKHealthStore.isHealthDataAvailable(),
let activeCaloriesBurned = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned) else {
return nil
}
return [activeCaloriesBurned, .workoutType()]
}
}

AppleHealthAuthorisationRequester

Another simple enum with two methods to request authorization and get the authorization status from HealthKit

enum AppleHealthAuthorisationRequester {
static func requestAuthorisation(onCompletion: @escaping () -> Void) {
guard let writeDataTypes = AppleHealthUsedDataTypeProvider.provideWriteTypes(),
let readDataTypes = AppleHealthUsedDataTypeProvider.provideReadTypes() else {
DispatchQueue.main.async {
onCompletion()
}
return
}

HKHealthStore().requestAuthorization(toShare: writeDataTypes, read: readDataTypes) { success, error in
DispatchQueue.main.async {
onCompletion()
}
}
}

static func requestStatusForAuthorisation(onCompletion: @escaping (HKAuthorizationRequestStatus) -> Void) {
guard let writeDataTypes = AppleHealthUsedDataTypeProvider.provideWriteTypes(),
let readDataTypes = AppleHealthUsedDataTypeProvider.provideReadTypes() else {
DispatchQueue.main.async {
onCompletion(.unknown)
}
return
}

HKHealthStore().getRequestStatusForAuthorization(toShare: writeDataTypes, read: readDataTypes) { status, error in
DispatchQueue.main.async {
onCompletion(status)
}
}
}
}

Great, now we can finally get started with implementing the view model! Enter, AppleHealthIntegrationViewModel

final class AppleHealthIntegrationViewModel: ObservableObject {
@Published var state: HealthKitIntegrationState = .loading

init() {
getAuthorisationStatusForAppleHealthDataTypes()
}

private func getAuthorisationStatusForAppleHealthDataTypes() {
// Will be implemented soon
}
}

For now its a simple view model with a published state property set to .loading at first, and a method called right after initialisation to get the authorization status.

Computing the HealthKit Authorization State

Now we are going to implement the body of getAuthorisationStatusForAppleHealthDataTypes to check the authorization status and update our state properly, with the needed associated values, but before doing that, we need to add a new property to AppleHealthWriteDataTypes we implemented earlier, to make our lives easier later on:

extension AppleHealthWriteDataTypes {
var isDetermined: Bool {
switch self {
case .activeCaloriesBurned(let authStatus),
.workout(let authStatus):
return authStatus != .notDetermined
}
}
}

isDetermined will be used to identify the determined types easily.

Okay now we can get back to implementing getAuthorisationStatusForAppleHealthDataTypes in the view model:

private func getAuthorisationStatusForAppleHealthDataTypes() {
guard let activeCaloriesBurned = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned) else {
state = .notDetermined
return
}

let healthStore = HKHealthStore()

// 2
// Workout Write Type
let workoutAuthorisationStatus = healthStore.authorizationStatus(for: .workoutType())
let workoutType: AppleHealthWriteDataTypes = .workout(workoutAuthorisationStatus)

// Active Calories Write Type
let activeCaloriesBurnedAuthorisationStatus = healthStore.authorizationStatus(for: activeCaloriesBurned)
let activeCaloriesBurnedType: AppleHealthWriteDataTypes = .activeCaloriesBurned(activeCaloriesBurnedAuthorisationStatus)

// 3
let appleHealthWriteTypes = [workoutType, activeCaloriesBurnedType]
let determinedWriteHealthTypes = appleHealthWriteTypes.filter { $0.isDetermined }
let nonDeterminedWriteHealthTypes = appleHealthWriteTypes.filter { $0.isDetermined == false }

// 4
AppleHealthAuthorisationRequester.requestStatusForAuthorisation { status in
self.updateState(
requestStatusForAuthorisation: status,
nonDeterminedWriteHealthTypes: nonDeterminedWriteHealthTypes,
determinedWriteHealthTypes: determinedWriteHealthTypes,
appleHealthWriteTypes: appleHealthWriteTypes
)
}
}

private func updateState(
requestStatusForAuthorisation: HKAuthorizationRequestStatus,
nonDeterminedWriteHealthTypes: [AppleHealthWriteDataTypes],
determinedWriteHealthTypes: [AppleHealthWriteDataTypes],
appleHealthWriteTypes: [AppleHealthWriteDataTypes]
) {
// 5
switch requestStatusForAuthorisation {
case .unknown:
// 6
state = .healthDataNotAvailable
case .unnecessary:
// 7
state = .determined(determinedWriteHealthTypes)
case .shouldRequest:
// 8
if determinedWriteHealthTypes.count == 0 {
state = .notDetermined
} else {
state = .partiallyDetermined(determinedWriteTypes: determinedWriteHealthTypes, nonDeterminedWriteTypes: nonDeterminedWriteHealthTypes)
}
}
}

Lets see what we do now step by step as indicated by the numbered comments:

  • 1) First we declare a HKQuantityType for the active calories burned, and declare a HKHealthStore
  • 2) Then we get the authorization status for our write types, and then set our custom types for further processing. You might be wondering, why we need an additional enum, and why we don’t just work with HKSampleTypes provided with HealthKit? It’s because HKSampleType is not an enum to easily identify what kind of sample type that is. Thus, we map them to our own health data type, AppleHealthWriteDataTypes we created earlier.
  • 3) Then we declare three different arrays, first, appleHealthWriteTypes that includes all the data types we want to write, then we declare determinedWriteHealthTypes, including only the types the user has already seen and determined its state (authorized or denied), by filtering the first array with recently created isDetermined value. Then the final one, nonDeterminedWriteHealthTypes, including the ones are not yet determined, meaning the user didn’t make a decision for those types yet.
  • 4) Then we ask the request status for authorization with our helper AppleHealthAuthorisationRequester and pass the status of type HKAuthorizationRequestStatus to updateState method, along with all the arrays we have declared earlier.
  • 5) In the updateState method, we switch on the requestStatusForAuthorisation
  • 6) If the case is .unknown, that means an error occurred during the retrieval of the auth status, then we can set our view’s state to healthDataNotAvailable (You could alternatively implement a separate state for this to show a different error, but we used the same state for when the health data is not available on the device)
  • 7) If the case is .unnecessary , that means for the given read and write types, the user has already made a decision, so we can set our state to .determined, with the determinedWriteTypes we have created earlier.
  • 8) Finally for the case .shouldRequest, we need to do a bit more, to understand, if we will request authorization for the first time, or if we did request authorization in the past, but now we need it again because we introduced additional data types at a later version. We understand it by looking at the determinedWriteHealthTypes count, if its greater than 0, that means there were some write types determined earlier, meaning we need to request additional health types, so we set our state to .partiallyDetermined, by also passing the determinedWriteHealthTypes and nonDeterminedWriteHealthTypes. If the count is 0 though, we set our state to .notDetermined

With this logic in place, we can now cover all cases we wanted to achieve. Please note that, since there is no way to identify if a data type for reading values from is authorized, if your app is only concerned with reading data from HealthKit, this approach won’t work. Similarly, also for Fit Records, if the user only enables reading data but disables writing data to HealthKit, the UI will look as if the user has denied all access. This is something we are fine with, given that there is no additional API to verify the status for read types, and our app really needs the write authorization to properly implement HealthKit integration :)

Before we move onto the view, we will implement one more thing for our view model, as you will see in the view examples in the next section, user can jump to the Health App to do modifications, and come back to our app. Since we show detailed write type authorization status, we need to make sure our view won’t show an outdated state when user comes back, in case they make any changes.

We will achieve this by observing the willEnterForeground notification and recomputing our state as follows:

Inside the init we will add the view model as an observer for the UIApplication.willEnterForegroundNotification notification:

NotificationCenter.default.addObserver(
self,
selector: #selector(appWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)

Then we will also implement the appWillEnterForeground method where we trigger getting authorization status:

@objc
private func appWillEnterForeground() {
getAuthorisationStatusForWriteTypes()
}

Thats it, now if user jumps to Health App and comes back to Fit Records, we will show the most up to date HealthKit integration state :)

Implementing the View

For brevity, we are not going to go into details of all the subviews, but we will briefly discuss their implementation details. Drop a comment if you would like another article showing the implementation details of the subviews though :)

Introducing AppleHealthIntegrationView

struct AppleHealthIntegrationView: View {

@StateObject var viewModel: AppleHealthIntegrationViewModel

var body: some View {
VStack {
VStack {
switch viewModel.state {
case .healthDataNotAvailable:
makeHealthDataNotAvailableView()
case .loading:
ProgressView()
case .notDetermined:
makeNonDeterminedView()
case .determined(let determinedTypes):
makeDeterminedView(determinedTypes: determinedTypes)
case .partiallyDetermined(let determinedTypes, let nonDeterminedTypes):
makePartiallyDeterminedView(determinedTypes: determinedTypes, nonDeterminedTypes: nonDeterminedTypes)
}
}
.padding()
.background(Color(uiColor: .systemGray6).cornerRadius(16.0))
.padding()
Spacer()
}
}
}

It is a simple view with one StateObject, the viewModel , then in its body, we switch on the viewModel’s state to draw our UI. Let’s discuss how and what we show for each state:

makeHealthDataNotAvailableView()

This view is shown when the health data is not available in user’s device, and it looks like the following:

makeNonDeterminedView()

This view is shown when the user has not interacted with Apple Health authorization yet, it shows some descriptive labels, and an Integrate button, triggering the authorization request to the system, by calling the previously implementedrequestAuthorisation method of AppleHealthAuthorisationRequester. It looks like the following:

makeDeterminedView(determinedTypes:)

This method takes an array of determined write types and provides one of the two views. If at least one of the write types is authorized, it shows the authorization status as following by using a ForEach for all the provided determined write types (Shows checkmark if authorized, and an xmark if denied):

But if the all the write types are denied, it shows the following view:

In order to easily identify whether at least one write type is authorized, we first need to add another extension to AppleHealthWriteDataTypes :

extension AppleHealthWriteDataTypes {
var isAuthorised: Bool {
switch self {
case .activeCaloriesBurned(let authStatus),
.workout(let authStatus):
return authStatus == .sharingAuthorized
}
}
}

And in AppleHealthIntegrationViewModel we need to add an internal method for the view to interact with, and a private method to check authorization status for each determined write type (The view could actually compute this with the data provided from viewModel’s state, but we kept these methods in the view model to keep the view as logic free as possible):

func isAtLeastOneWriteTypeAuthorised() -> Bool {
switch state {
case .healthDataNotAvailable, .loading, .notDetermined:
assertionFailure("This method shouldn't have been called for a state other than determined or partially determined!")
return false
case .determined(let healthTypes):
return isAtLeastOneTypeIsAuthorised(determinedTypes: healthTypes)
case .partiallyDetermined(let determinedTypes, _):
return isAtLeastOneTypeIsAuthorised(determinedTypes: determinedTypes)
}
}

private func isAtLeastOneTypeIsAuthorised(determinedTypes: [AppleHealthWriteDataTypes]) -> Bool {
for determinedType in determinedTypes {
if determinedType.isAuthorised {
return true
}
}
return false
}

makePartiallyDeterminedView(determinedTypes:, nonDeterminedTypes:)

As the last possible subview, this one shows the authorization status for the determined write types, and in a sub section, it shows the types that are yet to be determined, it uses two separate ForEach’s for both determinedTypes and nonDeterminedTypes, and a button to trigger the reauthorization flow for the new data types, and it looks like the following (Imagine we are introducing Height as a new type to write data in a future version):

Thats it! We have seen and discussed the views for all the possible states of a user in our health kit auhorisation management view. Although we omitted the implementation details for the views, with the APIs and the view model provided, you should be able to build a similar view for your apps in no time :)

Introducing a New Health Data Type at a Later Version

We have implemented the management view, and its view model, and also added for support for showing a section for new data types, that are yet to be determined in a subsection, along with a “Authorize Additional Access” button, but with the current set up, how can we introduce a new type, how easy it is?

Let’s have a look.

Imagine we want to introduce the Height as a new data type to write to HealthKit, to do that, we only need to update a few places in our code.

First, we need to update AppleHealthWriteDataTypes to include a new case for the height, and also edit its extensions of isDetermined and isAuthorised

enum AppleHealthWriteDataTypes: Hashable, Identifiable {
...
case height(HKAuthorizationStatus)
}

extension AppleHealthWriteDataTypes {
var isDetermined: Bool {
...
.height(let authStatus):
return authStatus != .notDetermined
}
}
}

extension AppleHealthWriteDataTypes {
var isAuthorised: Bool {
...
.height(let authStatus):
return authStatus == .sharingAuthorized
}
}
}

Then we need to update AppleHealthUsedDataTypeProvider to include height in the write types set:

enum AppleHealthUsedDataTypeProvider {
...

static func provideWriteTypes() -> Set<HKSampleType>? {
guard HKHealthStore.isHealthDataAvailable(),
let activeCaloriesBurned = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
let height = HKObjectType.quantityType(forIdentifier: .height) else {
return nil
}
return [activeCaloriesBurned, .workoutType(), height]
}
}

Then finally, in the getAuthorisationStatusForAppleHealthDataTypes method of AppleHealthIntegrationViewModel we include the new type as:

private func getAuthorisationStatusForWriteTypes() {
guard let activeCaloriesBurned = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
let height = HKObjectType.quantityType(forIdentifier: .height) else {
state = .notDetermined
return
}

...

// Height Write Type
let heightStatus = healthStore.authorizationStatus(for: height)
let heightType: AppleHealthWriteDataTypes = .height(heightStatus)

let appleHealthWriteTypes = [workoutType, activeCaloriesBurnedType, heightType]

...
}

Thats all! Now, if we run the app again for a user that has already authorized some write types, they will see the new type available same as the image shared for a partially determined view in the above section.

Final Words

In this piece, we have implemented a scalable HealthKit authorization state management logic, and also an accompanying view to inform users of their current state of HealthKit Integration, same as we did for Fit Records. We have made sure to show the authorization status for each write data type when determined, and added support for introducing new data types in the future, while also caring for the states where authorization is not determined, or denied.

I hope you found this article useful. Let me know what you think about it in the comments section. How are you managing Health Integration State? :)

Also if you are looking for a modern iOS App to track your workouts and exercises, give us a shot!

Until next time 👋

--

--

Emre Havan
Fit Records

Senior iOS Software Engineer — Interested in Compilers, ML and recommender systems — https://github.com/emrepun