Last active
April 5, 2019 23:16
-
-
Save converted2mac/a7e3159dcec59809116b69b64f6bbe5b to your computer and use it in GitHub Desktop.
Revisions
-
converted2mac revised this gist
Jun 19, 2017 . 1 changed file with 6 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or 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 charactersOriginal 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 -
converted2mac revised this gist
Jun 19, 2017 . No changes.There are no files selected for viewing
-
converted2mac revised this gist
Jun 19, 2017 . No changes.There are no files selected for viewing
-
converted2mac revised this gist
Jun 19, 2017 . No changes.There are no files selected for viewing
-
converted2mac created this gist
Jun 19, 2017 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal 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) } }