Created
March 3, 2021 01:07
-
-
Save msepcot/097aedc6a7b57b102219ea59290993f5 to your computer and use it in GitHub Desktop.
Search HealthKit data for running PRs.
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 characters
// | |
// ViewController.swift | |
// WatchAndLearn | |
// | |
// Created by Michael Sepcot on 9/18/19. | |
// Copyright © 2019 Michael Sepcot. All rights reserved. | |
// | |
import UIKit | |
import HealthKit | |
import CoreLocation | |
class ViewController: UIViewController { | |
var pr_5k: HKWorkout? | |
var pr_10k: HKWorkout? | |
var pr_halfMarathon: HKWorkout? | |
var pr_marathon: HKWorkout? | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
// Do any additional setup after loading the view. | |
// Run the query | |
guard let workoutRouteType = HKObjectType.seriesType(forIdentifier: HKWorkoutRouteTypeIdentifier), | |
let walkingRunningDistance = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning), | |
let steps = HKQuantityType.quantityType(forIdentifier: .stepCount), | |
let heartRate = HKQuantityType.quantityType(forIdentifier: .heartRate) | |
else { | |
fatalError("could not find workout route type") | |
} | |
HealthKitManager.sharedInstance.healthStore?.requestAuthorization(toShare: nil, read: [heartRate, steps, walkingRunningDistance, .workoutType(), workoutRouteType], completion: { (success, error) -> Void in | |
if success { | |
HealthKitManager.sharedInstance.healthStore?.execute(self.fiveK()) | |
HealthKitManager.sharedInstance.healthStore?.execute(self.tenK()) | |
HealthKitManager.sharedInstance.healthStore?.execute(self.halfMarathon()) | |
HealthKitManager.sharedInstance.healthStore?.execute(self.marathon()) | |
HealthKitManager.sharedInstance.healthStore?.execute(self.maxHR()) | |
} else { | |
print(error.debugDescription) | |
} | |
}) | |
} | |
func fiveK() -> HKQuery { | |
// 5K [4750, 5250] | |
let fiveKs = NSCompoundPredicate(andPredicateWithSubpredicates: [ | |
HKQuery.predicateForWorkouts(with: .running), | |
HKQuery.predicateForWorkouts(with: .greaterThan, totalDistance: HKQuantity.init(unit: .meter(), doubleValue: 4750)), | |
HKQuery.predicateForWorkouts(with: .lessThan, totalDistance: HKQuantity.init(unit: .meter(), doubleValue: 5250)) | |
]) | |
// Create the sort descriptor for most recent | |
let shortestTime = NSSortDescriptor(key: HKPredicateKeyPathWorkoutDuration, ascending: true) | |
// Build the query | |
let query = HKSampleQuery(sampleType: .workoutType(), predicate: fiveKs, limit: 1, sortDescriptors: [shortestTime]) { (query, results, error) in | |
guard let workouts = results else { fatalError("*** Query Failed ***") } | |
let workout = workouts.first as! HKWorkout | |
self.pr_5k = workout | |
print("*** 5K PR: \(PaceCalculator.hhmmss(workout.duration)) ***") | |
} | |
return query | |
} | |
func tenK() -> HKQuery { | |
// 10K [9500, 10500] | |
let tenKs = NSCompoundPredicate(andPredicateWithSubpredicates: [ | |
HKQuery.predicateForWorkouts(with: .running), | |
HKQuery.predicateForWorkouts(with: .greaterThan, totalDistance: HKQuantity.init(unit: .meter(), doubleValue: 9500)), | |
HKQuery.predicateForWorkouts(with: .lessThan, totalDistance: HKQuantity.init(unit: .meter(), doubleValue: 10500)) | |
]) | |
// Create the sort descriptor for most recent | |
let shortestTime = NSSortDescriptor(key: HKPredicateKeyPathWorkoutDuration, ascending: true) | |
// Build the query | |
let query = HKSampleQuery(sampleType: .workoutType(), predicate: tenKs, limit: 1, sortDescriptors: [shortestTime]) { (query, results, error) in | |
guard let workouts = results else { fatalError("*** Query Failed ***") } | |
let workout = workouts.first as! HKWorkout | |
self.pr_10k = workout | |
print("*** 10K PR: \(PaceCalculator.hhmmss(workout.duration)) ***") | |
} | |
return query | |
} | |
func halfMarathon() -> HKQuery { | |
// 13.1mi (21.0975km) [20043, 22152] | |
let halfMarathons = NSCompoundPredicate(andPredicateWithSubpredicates: [ | |
HKQuery.predicateForWorkouts(with: .running), | |
HKQuery.predicateForWorkouts(with: .greaterThan, totalDistance: HKQuantity.init(unit: .meter(), doubleValue: 20043)), | |
HKQuery.predicateForWorkouts(with: .lessThan, totalDistance: HKQuantity.init(unit: .meter(), doubleValue: 22152)) | |
]) | |
// Create the sort descriptor for most recent | |
let shortestTime = NSSortDescriptor(key: HKPredicateKeyPathWorkoutDuration, ascending: true) | |
// Build the query | |
let query = HKSampleQuery(sampleType: .workoutType(), predicate: halfMarathons, limit: 1, sortDescriptors: [shortestTime]) { (query, results, error) in | |
guard let workouts = results else { fatalError("*** Query Failed ***") } | |
let workout = workouts.first as! HKWorkout | |
self.pr_halfMarathon = workout | |
print("*** Half Marathon PR: \(PaceCalculator.hhmmss(workout.duration)) ***") | |
} | |
return query | |
} | |
func marathon() -> HKQuery { | |
// 26.2mi (42.195km) [40085, 44305] | |
let marathons = NSCompoundPredicate(andPredicateWithSubpredicates: [ | |
HKQuery.predicateForWorkouts(with: .running), | |
HKQuery.predicateForWorkouts(with: .greaterThan, totalDistance: HKQuantity.init(unit: .meter(), doubleValue: 40085)), | |
HKQuery.predicateForWorkouts(with: .lessThan, totalDistance: HKQuantity.init(unit: .meter(), doubleValue: 44305)) | |
]) | |
// Create the sort descriptor for most recent | |
let shortestTime = NSSortDescriptor(key: HKPredicateKeyPathWorkoutDuration, ascending: true) | |
// Build the query | |
let query = HKSampleQuery(sampleType: .workoutType(), predicate: marathons, limit: 1, sortDescriptors: [shortestTime]) { (query, results, error) in | |
guard let workouts = results else { fatalError("*** Query Failed ***") } | |
let workout = workouts.first as! HKWorkout | |
self.pr_marathon = workout | |
print("*** Marathon PR: \(PaceCalculator.hhmmss(workout.duration)) ***") | |
} | |
return query | |
} | |
func estimate() { | |
} | |
func findDistanceNotAssociatedWithRun() -> HKQuery { | |
// Create the predicate for the query | |
let runningWorkouts = HKQuery.predicateForWorkouts(with: .running) | |
// Create the sort descriptor for most recent | |
let mostRecent = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) | |
// Build the query | |
let query = HKSampleQuery(sampleType: .workoutType(), predicate: runningWorkouts, limit: 2, sortDescriptors: [mostRecent]) { (query, results, error) in | |
guard let samples = results else { | |
guard error != nil else { | |
fatalError("*** Did not return a valid error object. ***") | |
} | |
// Handle the error here... | |
print(error.debugDescription) | |
return | |
} | |
// Do something with the summaries here... | |
for myWorkout in samples { | |
print(myWorkout) | |
guard let distanceType = HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.distanceWalkingRunning) else { | |
fatalError("*** Unable to create the distance type ***") | |
} | |
let summariesWithinRange = HKQuery.predicateForSamples(withStart: myWorkout.startDate, end: myWorkout.endDate, options: [.strictStartDate, .strictEndDate]) | |
let startDateSort = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true) | |
let query = HKSampleQuery(sampleType: distanceType, predicate: summariesWithinRange, limit: 0, sortDescriptors: [startDateSort]) { | |
(sampleQuery, results, error) -> Void in | |
guard let quantitySamples = results as? [HKQuantitySample] else { | |
// Perform proper error handling here... | |
fatalError("*** An error occurred while adding a sample to " + | |
"the workout: \(String(describing: error?.localizedDescription))") | |
} | |
// process the detailed samples... | |
print(quantitySamples.count) | |
} | |
HealthKitManager.sharedInstance.healthStore?.execute(query) | |
} | |
} | |
return query | |
} | |
func routes() -> HKQuery { | |
let runningWorkouts = HKQuery.predicateForWorkouts(with: .running) | |
// Create the sort descriptor for most recent | |
let mostRecent = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false) | |
// Build the query | |
let query = HKSampleQuery(sampleType: .workoutType(), predicate: runningWorkouts, limit: 2, sortDescriptors: [mostRecent]) { (query, results, error) in | |
guard let samples = results else { | |
guard error != nil else { | |
fatalError("*** Did not return a valid error object. ***") | |
} | |
// Handle the error here... | |
print(error.debugDescription) | |
return | |
} | |
// Do something with the summaries here... | |
for myWorkout in samples { | |
let runningObjectQuery = HKQuery.predicateForObjects(from: myWorkout as! HKWorkout) | |
let routeQuery = HKAnchoredObjectQuery(type: HKSeriesType.workoutRoute(), predicate: runningObjectQuery, anchor: nil, limit: HKObjectQueryNoLimit) { (query, samples, deletedObjects, anchor, error) in | |
guard error == nil else { | |
// Handle any errors here. | |
fatalError("The initial query failed.") | |
} | |
guard let routes = samples else { | |
return | |
} | |
for route in routes { | |
// Create the route query. | |
let locationQuery = HKWorkoutRouteQuery(route: route as! HKWorkoutRoute) { (query, locationsOrNil, done, errorOrNil) in | |
// This block may be called multiple times. | |
if let error = errorOrNil { | |
print(error) | |
return | |
} | |
guard let locations = locationsOrNil else { | |
fatalError("*** Invalid State: This can only fail if there was an error. ***") | |
} | |
// Do something with this batch of location data. | |
print(locations.count) | |
} | |
HealthKitManager.sharedInstance.healthStore?.execute(locationQuery) | |
} | |
} | |
HealthKitManager.sharedInstance.healthStore?.execute(routeQuery) | |
} | |
} | |
return query | |
} | |
func lastWeeksSummaries() -> HKQuery { | |
// Create the date components for the predicate | |
let calendar = Calendar(identifier: Calendar.Identifier.gregorian) | |
let endDate = Date() | |
guard let startDate = calendar.date(byAdding: .day, value: -7, to: endDate, wrappingComponents: true) else { | |
fatalError("*** unable to calculate the start date ***") | |
} | |
let units: Set<Calendar.Component> = [.day, .month, .year, .era] | |
var startDateComponents = calendar.dateComponents(units, from: startDate) | |
startDateComponents.calendar = calendar | |
var endDateComponents = calendar.dateComponents(units, from: endDate) | |
endDateComponents.calendar = calendar | |
let summariesWithinRange = HKQuery.predicate(forActivitySummariesBetweenStart: startDateComponents, end: endDateComponents) | |
let query = HKActivitySummaryQuery(predicate: summariesWithinRange) { (query, summaries, error) -> Void in | |
guard let activitySummaries = summaries else { | |
guard error != nil else { | |
fatalError("*** Did not return a valid error object. ***") | |
} | |
// Handle the error here... | |
print(error.debugDescription) | |
return | |
} | |
// Do something with the summaries here... | |
for summary in activitySummaries { | |
print(summary) | |
} | |
} | |
return query | |
} | |
func maxHR() -> HKQuery { | |
let query = HKStatisticsQuery( | |
quantityType: HKQuantityType.quantityType(forIdentifier: .heartRate)!, | |
quantitySamplePredicate: nil, | |
options: .discreteMax | |
) { | |
query, statisticsOrNil, errorOrNil in | |
if let value = statisticsOrNil?.maximumQuantity() { | |
let bpm = value.doubleValue(for: HKUnit.count().unitDivided(by: .minute())) | |
print("Max value is \(bpm)") | |
} | |
} | |
return query | |
} | |
} | |
class PaceCalculator { | |
typealias Seconds = Int | |
enum Distance: Double { // in miles | |
case matathon = 26.21875 | |
case halfMarathon = 13.109375 | |
case tenMile = 10 | |
case tenKilometer = 6.21371192 | |
case fiveMile = 5 | |
case fiveKilometer = 3.10685596 | |
case threeKilometer = 1.864113576 | |
case oneMile = 1 | |
case fifteenHundredMeter = 0.932056788 | |
} | |
struct Race { | |
let distance: Distance | |
let time: Seconds | |
init(distance: Distance, time: Seconds) { | |
self.distance = distance | |
self.time = time | |
} | |
init(distance: Distance, hours: Int, minutes: Int, seconds: Int) { | |
self.distance = distance | |
self.time = PaceCalculator.seconds(fromHours: hours, minutes: minutes, seconds: seconds) | |
} | |
func pace() -> Double { // seconds per mile | |
return Double(time) / distance.rawValue | |
} | |
} | |
// var goalRaceDistance: Distance | |
var recentRace: Race | |
var anotherRace: Race? | |
var milesPerWeek: Int | |
init(recentRace: Race, milesPerWeek: Int) { | |
self.recentRace = recentRace | |
self.milesPerWeek = milesPerWeek | |
} | |
init(recentRaceDistance: Distance, recentRaceTime: Seconds, milesPerWeek: Int) { | |
self.recentRace = Race(distance: recentRaceDistance, time: recentRaceTime) // pace 210 - 960 | |
self.milesPerWeek = milesPerWeek // 5 - 150 | |
} | |
func calculate(forDistance distance: Distance) -> Int? { | |
guard recentRace.distance != distance else { return nil } // TODO throw warning? | |
// Marathon based on one race | |
let meters = recentRace.distance.rawValue * 1609.34 | |
let velocityRiegel = 42195 / (Double(recentRace.time) * pow(42195 / meters, 1.07)) | |
let velocityModel = 0.16018617 + 0.83076202 * velocityRiegel + 0.06423826 * (Double(milesPerWeek) / 10.0) | |
let predictedTime = 42195 / 60 / velocityModel * 60 | |
return Int(predictedTime) | |
} | |
// MARK: - Helper Methods | |
class func hhmmss(_ time: TimeInterval) -> String { | |
let time = Int(time) | |
let hours = time / 3600 | |
let seconds = time % 60 | |
let minutes = (time - (hours * 3600)) / 60 | |
return String(format: "%02i:%02i:%02i", hours, minutes, seconds) | |
} | |
class func seconds(fromHours hours: Int, minutes: Int, seconds: Int) -> Int { | |
return seconds + minutes * 60 + hours * 3600 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Dummy view controller to run on a device, outputs to the console log.