Kurt Frey
aka
NitricWare
# Adding Sexual Activity to Apple Health via HealthKit
26.08.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:

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.