DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 968,547 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Kurt HΓΆblinger
Kurt HΓΆblinger

Posted on

Adding Sexual Activity to Apple Health via HealthKit

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

Modify a sample

You'll have to delete the old object and save a new sample.

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.