Skip to content

Instantly share code, notes, and snippets.

@converted2mac
Last active April 5, 2019 23:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save converted2mac/a7e3159dcec59809116b69b64f6bbe5b to your computer and use it in GitHub Desktop.
Save converted2mac/a7e3159dcec59809116b69b64f6bbe5b to your computer and use it in GitHub Desktop.
Drop-in wrapper around fetching data from HealthKit, written in Swift 3. Not open source, but I have permission to use for portfolio.
//
// Created by Daniel James on 3/1/17.
//
// At the time of original writing, I only needed to support three data types,
// but wanted to be able to easily add additional types later. As such, adding
// a supported type should be as simple as including the new type in the
// ActivityTypeOptions OptionSet and an if-statement to check for that type in
// the healthKitActivityTypes() function.
//
import HealthKit
class ActivityKit : NSObject {
//MARK: Internal Properties
let healthStore = HKHealthStore()
//MARK: ActivityKit Error info
let kErrorDomain = "org.caloriecloud.healthkit"
enum kActivityKitError: Int {
case NoStatisticsReturned = 1001
case HealthKitUnavailable = 1002
case NoStatisticsRequested = 1003
}
//MARK: External properties
public static let sharedInstance = ActivityKit()
/// OptionSet describing a combination of activity types that are supported by ActivityKit (Swift-only access, since backed by OptionSet)
public struct ActivityTypeOptions : OptionSet {
let rawValue: Int
static let steps = ActivityTypeOptions(rawValue: 1 << 0)
static let activeCalories = ActivityTypeOptions(rawValue: 1 << 1)
static let exerciseMinutes = ActivityTypeOptions(rawValue: 1 << 2)
static let allOptions: ActivityTypeOptions = [.steps, .activeCalories, .exerciseMinutes]
}
/// Bitmask containing the Activity Types to query; defaults to all
public var activityTypeOptions: ActivityTypeOptions = .allOptions
/// The minute interval between HealthKit data objects; defaults to 15
public var dataInterval = 15
//MARK: External functions
/// Function used to prompt the user for authorization to their HealthKit data
public func authorizeHealthKit(completion: ((_ success: Bool, _ error: NSError?) -> Void)?) {
guard self.isHealthKitAvailable() == true else {
let error = NSError(domain: self.kErrorDomain, code: kActivityKitError.HealthKitUnavailable.rawValue, userInfo: nil)
completion?(false, error)
return
}
guard let healthTypes = self.healthKitActivityTypes() else {
NSLog("No health types defined. Don't ask permission")
let error = NSError(domain: self.kErrorDomain, code: kActivityKitError.NoStatisticsRequested.rawValue, userInfo: nil)
completion?(false, error)
return
}
// Request authorization to read/write the specified data types above
self.healthStore.requestAuthorization(toShare: nil, read: healthTypes) { (success: Bool, error: Error?) -> Void in
DispatchQueue.main.async {
completion?(success, error as NSError?)
}
}
}
/// Return a set of HKQuantityTypes based on the configurable OptionSet
public func healthKitActivityTypes() -> Set<HKObjectType>? {
var healthDataToRead = Set<HKObjectType>()
if self.activityTypeOptions.contains(.steps) {
healthDataToRead.insert(HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!)
}
if self.activityTypeOptions.contains(.activeCalories) {
healthDataToRead.insert(HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.activeEnergyBurned)!)
}
if #available(iOS 9.3, *) { //exercise time not added until iOS 9.3, so guard against this
if self.activityTypeOptions.contains(.exerciseMinutes) {
healthDataToRead.insert(HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.appleExerciseTime)!)
}
}
return healthDataToRead.count > 0 ? healthDataToRead : nil
}
/// Check if HealthKit is available on device
public func isHealthKitAvailable() -> Bool {
return (HKHealthStore.isHealthDataAvailable() ? true : false)
}
/// Get all activity data as specified by 'activityTypeOptions'. Public entry point for calling a query of the HK database.
///
/// - Parameters:
/// - start: Start date determining the beginning point for which statistics will be returned
/// - completion: A block to run when the query finishes. Dictionary, if present returns arrays of HKStatistics objects that are keyed using HKIdentifier for each quantity type.
public func getActivityData(since start: Date?, completion:(([String: Array<HKStatistics>]?, NSError?) -> Void)?) {
//make sure we're ready to get all specified activity types
guard let healthTypes = self.healthKitActivityTypes() else {
NSLog("Not set to read any health data. Set `activityTypeOptions` property")
let error = NSError(domain: self.kErrorDomain, code: kActivityKitError.NoStatisticsRequested.rawValue, userInfo: nil)
completion?(nil, error)
return
}
//set date bounds
let endDate = Date()
let startDate = start ?? Calendar.current.date(byAdding: .day, value: -30, to: endDate)!
//prep for response
var resultsDictionary = [String: Array<HKStatistics>]()
//Dispatch group to wait for multiple async responses
let queryGroup = DispatchGroup()
for typeToRead in healthTypes {
queryGroup.enter()
self.queryForActivity(since: startDate, to: endDate, for: (typeToRead as! HKQuantityType), completion: { statisticsArray, error in
if statisticsArray != nil {
resultsDictionary[typeToRead.identifier] = statisticsArray
}
queryGroup.leave()
})
}
queryGroup.notify(queue: DispatchQueue.main) {
if resultsDictionary.count > 0 {
completion?(resultsDictionary, nil)
} else {
let error = NSError(domain: self.kErrorDomain, code: kActivityKitError.NoStatisticsReturned.rawValue, userInfo: nil)
completion?(nil, error)
}
}
}
//MARK: Internal Helpers
/// Query for a specific data type. Only used privately inside this class.
///
/// - Parameters:
/// - startDate: start of range for which to query
/// - endDate: end of range for which to query
/// - quantityType: type of data to query
/// - completion: block to run with results or error. Will not return an empty array; array is either populated or nil
private func queryForActivity(since startDate: Date, to endDate: Date, for quantityType: HKQuantityType, completion: ((Array<HKStatistics>?, NSError?) -> Void)?) {
var interval = DateComponents()
interval.minute = self.dataInterval
//construct query
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
let anchorDate = NSCalendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: endDate)! //establish anchor date with 00:00 so intervals occur precisely
let query = HKStatisticsCollectionQuery(quantityType: quantityType, quantitySamplePredicate: predicate, options: .cumulativeSum, anchorDate: anchorDate, intervalComponents: interval)
//handle results from the initial query
query.initialResultsHandler = { collectionQuery, queryResults, error in
guard let statsCollection = queryResults else {
NSLog("Error fetching results: \(error)")
completion?(nil, error as NSError?)
return
}
if statsCollection.statistics().isEmpty == false {
completion?(statsCollection.statistics(), nil)
} else {
completion?(nil, nil)
}
}
healthStore.execute(query)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment