Skip to content

Instantly share code, notes, and snippets.

@msepcot
Created March 3, 2021 01:07
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save msepcot/097aedc6a7b57b102219ea59290993f5 to your computer and use it in GitHub Desktop.
Save msepcot/097aedc6a7b57b102219ea59290993f5 to your computer and use it in GitHub Desktop.
Search HealthKit data for running PRs.
//
// 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
}
}
@msepcot
Copy link
Author

msepcot commented Mar 3, 2021

Dummy view controller to run on a device, outputs to the console log.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment