Kurt Frey
Posted on August 26, 2022
There are many examples on how to add heart rate or step counts to Apple Health via HealthKit. I made an app that lets you track your sexual activity. And now I'd like to add HealthKit support.
Prerequisites
After creating the app in Xcode, add the HealthKit capability and add the string to your Info.plist.
For clarity and better practice we'll create a class that'll handle the connection to HealthKit. You could add this to a view too. It would be ugly.
Sample vs. Object
A sample is what is written to HKHealthStore
and an object is what is read from HKHealthStore
.
HealthConnector
The custom HealthConnector
class facilitates interaction with HKHealthStore
. It currently has two three functions:
requestAuthorization
saveSample
deleteObject
Request authorization
Before any interaction with HKHealthStore
we must ask for permission. It's not enough to ask once. The user could revoke permission when we're not looking.
You have to specifically ask for the Sample Types you'd like to read from and write to. Since my app only writes samples but won't retrieve anything, requestAuthorization
's read
-parameter is nil
.
/// This function requests permission to read and/or write to a specific
/// `HKObjectType`.
/// - Parameter completion: Completion handler must be able to handle boolean return.
func requestAuthorization(completion: @escaping (Bool) -> Void) {
// Check if `HKHealthStore` is available to the user of the app.
guard HKHealthStore.isHealthDataAvailable() else {
completion(false)
return
}
// Check if category type `.sexualActivity` is available to the user.
guard let sexualActivityType = HKObjectType.categoryType(forIdentifier: .sexualActivity) else {
completion(false)
return
}
// Create a set containing the category types we'd like to access.
let writeTypes: Set = [
sexualActivityType
]
// Finally request authorization.
HKHealthStore().requestAuthorization(toShare: writeTypes, read: nil) {
(success, _) in
completion(success)
}
}
Save a sample
/// This function saves a new sample to `HKHealthStore`. It adds all neccessary meta date.
/// - Parameters:
/// - protection: Was protection used?
/// - date: When was the date of sexual acitivty?
/// - identifier: A UUID that allows to delete the sample later.
/// - completion: Completion handler must be able to handle boolean return.
func saveSample(with protection: Bool, date: Date, identifier: UUID, completion: @escaping (Bool) -> Void) {
// Check if category type `.sexualActivity` is available to the user.
guard let sexualActivityType = HKObjectType.categoryType(forIdentifier: .sexualActivity) else {
completion(false)
return
}
/**
Create the sample.
`value` must be 0. Otherwise the app will crash.
`metadata` must contain all three keys. `HKMetadataKeySyncIdentifier` requires `HKMetadataKeySyncVersion`. Version number is
*/
let sample = HKCategorySample(
type: sexualActivityType,
value: 0,
start: date,
end: date,
metadata: [
HKMetadataKeySexualActivityProtectionUsed: protectionUsed,
HKMetadataKeySyncIdentifier: identifier.uuidString,
HKMetadataKeySyncVersion: 1
]
)
HKHealthStore().save(sample) { (success, error) in
completion(success)
}
}
Delete an object
/// Deletes the sample with the specified identifier.
/// - Parameters:
/// - identifier: The UUID used to save the sample.
/// - completion: Completion handler must be able to handle boolean return.
func deleteObject(identifier: UUID, completion: @escaping (Bool) -> Void) {
// Narrow down the possible results.
let predicate = HKQuery.predicateForObjects(
withMetadataKey: HKMetadataKeySyncIdentifier,
allowedValues: [identifier.uuidString]
)
// Delete all the objects that match the predicate, which should only be one.
HKHealthStore().deleteObjects(of: HKCategoryType(.sexualActivity), predicate: predicate) { success, _, error in
completion(success)
}
}
Modify a sample
You'll have to delete the old object and save a new sample.
Posted on August 26, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.