Skip to content

Instantly share code, notes, and snippets.

@converted2mac
Last active April 5, 2019 23:16
Show Gist options
  • Select an option

  • Save converted2mac/a7e3159dcec59809116b69b64f6bbe5b to your computer and use it in GitHub Desktop.

Select an option

Save converted2mac/a7e3159dcec59809116b69b64f6bbe5b to your computer and use it in GitHub Desktop.

Revisions

  1. converted2mac revised this gist Jun 19, 2017. 1 changed file with 6 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions ActivityKit.swift
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,12 @@
    //
    // 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

  2. converted2mac revised this gist Jun 19, 2017. No changes.
  3. converted2mac revised this gist Jun 19, 2017. No changes.
  4. converted2mac revised this gist Jun 19, 2017. No changes.
  5. converted2mac created this gist Jun 19, 2017.
    177 changes: 177 additions & 0 deletions ActivityKit.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,177 @@
    //
    // Created by Daniel James on 3/1/17.
    //

    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)
    }
    }