Last active
April 5, 2019 23:16
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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