Writing HealthKit Queries
Lets have a chat about fetching data from HealthKit
Asking for permissions
A key tenant of health data is that you must always ask for permission to access the data. This is true for anything stored in HealthKit.
Thankfully SwiftUI makes it super easy to achieve this permission request as part of the HealthKitUI
framework that can be imported into your app to get access to the view modifiers.
Apple’s documentation for authorization and access is available at https://developer.apple.com/documentation/healthkit/authorizing-access-to-health-data and covers a lot of what needs to happen and why.
The first step is to make sure that your info.plist is updated with a value for NSHealthUpdateUsageDescription and NSHealthShareUsageDescription which are values for writing data (share) and reading data (receive updates).
Once those values are updated, it is time to make the requests for permissions. You don’t need to specify everything and if you add in more then you can prompt the user for access to just those without worrying about everything else.
It’s best to define a property somewhere that you can get access to and use this for specifying the types you want access to.
public var healthTypes: Set<HKObjectType> {
Set(
[
HKObjectType.activitySummaryType(),
HKObjectType.quantityType(forIdentifier: .heartRate),
HKQuantityType.quantityType(forIdentifier: .distanceCycling),
HKSeriesType.workoutRoute(),
HKObjectType.workoutType(),
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
HKObjectType.quantityType(forIdentifier: .cyclingCadence),
HKObjectType.quantityType(forIdentifier: .cyclingPower),
HKObjectType.quantityType(forIdentifier: .cyclingFunctionalThresholdPower),
HKObjectType.quantityType(forIdentifier: .cyclingSpeed),
HKObjectType.stateOfMindType(),
HKObjectType.quantityType(forIdentifier: .timeInDaylight),
].compacted()
)
}
You’ll notice that there are a few different types here being a combination of quantity, activity summary, workout, series and stateOfMind. These are all subclasses of HKObjectType
.
To check if you need to display the permissions prompt, you want to make use of the getRequestStatusForAuthorization(toShare:read:completion:) function on HKStore
. Thankfully this function also has a swift concurrency counterpart so it can be used as the following:
public var healthAuthorizationStatus: HKAuthorizationRequestStatus {
get async throws {
try await store.statusForAuthorizationRequest(toShare: shareTypes, read: readTypes)
}
}
The return type HKAuthorizationRequestStatus is automatically translated from Objective-C into Swift and has the following signature:
public enum HKAuthorizationRequestStatus : Int, @unchecked Sendable {
case unknown = 0
case shouldRequest = 1
case unnecessary = 2
}
The value we are interested in there is shouldRequest
as that tells us that we need to ask the user for permissions. This is what will drive your UI in showing the permission prompt. The function you want to call is healthDataAccessRequest(store:shareTypes:readTypes:trigger:completion:).
.healthDataAccessRequest(
store: viewModel.healthKitStore,
shareTypes: viewModel.healthKitShareTypes,
readTypes: viewModel.healthKitReadTypes,
trigger: viewModel.showConnectionPrompt
) { result in
switch result {
case .success:
authenticated = true
case .failure(let error):
// Handle the error here.
authenticated = false
}
}
Congratulations! You are now in a position where you have prompted the user for authorization to update and share data in HealthKit.
Before you start writing data to HealthKit you want to make sure that you have permission to do so. For that you use authorizationStatus(for:) and if that returns sharingAuthorized
you are good to go ahead and write the data.
Unfortunately there is no counterpart for checking if you can read so you need to use getRequestStatusForAuthorization(toShare:read:completion:)
. That though will only tell you if the user has been prompted for authorization to the specified read types. You can then check for data by querying HealthKit and if there is data for a specific range (today, last week, etcetera that’s within reason) you are good to.
Creating a predicate
The first step in performing a query is to construct a predicate. This is what will tell us the type of data we are dealing with and can get super confusing for those who haven’t worked with older iOS or macOS apps that have heavy CoreData interactions. The main types you’ll want to be familiar with is NSPredicate and HKSamplePredicate. Thankfully you don’t need to work with NSPredicate
directly but can use helper functions on HKQuery such as predicateForSamples(withStart:end:options:) and predicate(forActivitySummariesBetweenStart:end:).
These initial predicates end up looking like the following:
let predicate = HKQuery.predicateForSamples(
withStart: startDate,
end: .now,
options: .strictStartDate
)
They solve the problem of how to scope a query to a particular start and end date range. Though you also want to create a predicate that will allow for specifying a particular sample type. On HKSamplePredicate
there are functions such as quantitySample(type:predicate:) and sample(type:predicate:) that allow for creation of predicates for the required type. These look like the following:
let samplePredicate = HKSamplePredicate.quantitySample(
type: type,
predicate: predicate
)
Sort Descriptors
When making queries, even more so when dealing with health data, you want responses to be sorted in some way. Doing this is simple by creating a SortDescriptor instance.
let sortDescriptor = SortDescriptor(\HKSample.endDate, order: .forward)
Constructing a query descriptor
Now we are at a point where we can make the request to HealthKit to get the data. To do so we make use of query descriptors such as HKSampleQueryDescriptor and HKStatisticsCollectionQueryDescriptor. These work with swift concurrency by calling the result(for:) function on the descriptor.
let descriptor = HKSampleQueryDescriptor(
predicates: [
samplePredicate,
],
sortDescriptors: [
sortDescriptor
],
limit: HKObjectQueryNoLimit
)
let result = try await descriptor.result(for: store)
Working with the results
You’ve made it! You now have an array of HKSample
, HKQuantitySample
or other type of values based on the query that you’ve constructed. From here it is up to you to decide how you want to work with the values. There are a few tips though when dealing with this.
If you are working with quantity samples, it’s always important to show these values in the units that are appropriate for the device running the app. You can make use of the preferredUnits(for:completion:) function on HKStore
to achieve this.
That’s all for now. More posts to come soon as parts of HealthKitty get built.