Skip to content

Combine-based wrapper for common HealthKit operations

License

Notifications You must be signed in to change notification settings

javierdemartin/HKCombine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

platforms platforms code-size

HKCombine

Combine-based wrapper to perform HealthKit related queries.

My app, Singlet, makes full use of this Swift Package.

Installation

Add the repository link as a dependency on Xcode from File, Swift Packages & Add Package Dependency...

Usage

This package makes extensive use of the Combine framework.

Check if the device needs to request HealthKit authorization.
HKHealthStore()
    .needsAuthorization(for: TYPES_TO_QUERY, toShare: false, toRead: true)
    .replaceError(with: false)
    .sink(receiveValue: { needsAuthorization in
        
        /// Perform an action based on the result
        requestPermissionButtonEnabled = !needsAuthorization
        
    }).store(in: &cancellableBag)
Request permission to HealthKit for the given types.
HKHealthStore()
    .requestAuthorization(for: TYPES_TO_QUERY, toShare: false, toRead: true)
    .replaceError(with: false)
    .sink(receiveValue: { finished in
        
        /// Finish the authorization process
        presentMainScreen = true
    }).store(in: &cancellableBag)
Query HealthKit samples.
HKHealthStore()
    .get(sample: SAMPLE_TYPE, start: START_RANGE, end: END_RANGE)
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { subscription in
        /// Do something at the subscriber's end of life or error
    }, receiveValue: { samples in
        /// Save samples or do something with them
    }).store(in: &cancellableBag)
Query HKWorkout with all the associated heart rate and location data.

You can also query for a number of samples instead of using a Date range.

Bear in mind that this is an expensive request as it requests both heart rate data and the workout's route from every requested HKWorkout.

var samples: [HKCWorkoutDetails] = []

HKHealthStore()
    .workouts(type: .running, from: START_RANGE, to: END_RANGE)
    .flatMap({ $0.publisher })
    .flatMap({ $0.workoutWithDetails })
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { comp in
        switch comp {
        case .finished:
            /// `samples` contains all the data asked for
        case .failure(_):
            /// Act on the error
        }
    }, receiveValue: { details in
        samples.append(details)
    })
    .store(in: &cancellableBag)
HKStatisticQuery

The gist of this is to replace the possible error that might surface if the queried sample doesn't have permissions for it with a nil, or whatever suits your purpose, before continuing.

HKHealthStore()
    .statistic(for: HKObjectType.quantityType(forIdentifier: .restingHeartRate)!, with: .discreteAverage, from: Date().startOfMonth!, to: Date())
    .map({ $0.averageQuantity()?.doubleValue(for: UserUnits.shared().heartCountUnits) })
    .replaceError(with: nil)
    .assertNoFailure()
    .receive(on: DispatchQueue.main)
    .assign(to: &$VARIABLE)
Splits/Paces from a HKWorkout

If the HKWorkout you're querying has been recorded from an Apple Watch using the native Workouts.app this is straightforward.

workout.appleWatchPaces
    .receive(on: DispatchQueue.main)
    .replaceError(with: []) 
    .sink(receiveCompletion: { sub in
        
        switch sub {
        
        case .finished:
            break
        case .failure(_):
            fatalError()
        }
    },receiveValue: { events in
        
        /// Work with the received `HKWorkoutEvents`
        
    }).store(in: &bag)

There are other times when you want to query paces from an Apple Watch if it exists and default to manual calculations if it fails or they don't exist. This requires access to query .distanceWalkingRunning samples.

NOTE: These manual calculations, splits might have some errors as this is an algorithm where some issues might appear.

NOTE 2: Apps like Strava might not produce reliable calculations by the way the save data on HealthKit. As far as I know there is no workaround around this. If you have a better solution for this feel free to open a pull request.

workout.appleWatchPaces
    .receive(on: DispatchQueue.main)
    .replaceError(with: [])
    .flatMap({ applePaces -> AnyPublisher<[HKWorkoutEvent], Error> in
        
        if applePaces.isEmpty {
            return workout.workout.splits
        } else {
            return Just(applePaces).setFailureType(to: Error.self).eraseToAnyPublisher()
        }
    })
    
    .sink(receiveCompletion: { sub in
        
        switch sub {
        
        case .finished:
            break
        case .failure(_):
            fatalError()
        }
    },receiveValue: { events in
        
        /// Work with the `HKWorkoutEvents`
        
    }).store(in: &bag)

Disclaimers

Strava

About

Combine-based wrapper for common HealthKit operations

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages