Search code examples
swifthealthkit

HealthKit Running Splits In Kilometres Code Inaccurate – Why?


So below is the code that I've got thus far, cannot figure out why I'm getting inaccurate data.

Not accounting for the pause events yet that should not affect the first two kilometre inaccuracies...

So the output would be the distance 1km and the duration that km took. Any ideas for improvement, please help?

func getHealthKitWorkouts(){

    print("HealthKit Workout:")

    /* Boris here: Looks like we need some sort of Health Kit manager */
    let healthStore:HKHealthStore = HKHealthStore()
    let durationFormatter = NSDateComponentsFormatter()
    var workouts = [HKWorkout]()

    // Predicate to read only running workouts
    let predicate =  HKQuery.predicateForWorkoutsWithWorkoutActivityType(HKWorkoutActivityType.Running)
    // Order the workouts by date
    let sortDescriptor = NSSortDescriptor(key:HKSampleSortIdentifierStartDate, ascending: false)
    // Create the query
    let sampleQuery = HKSampleQuery(sampleType: HKWorkoutType.workoutType(), predicate: predicate, limit: 0, sortDescriptors: [sortDescriptor])
        { (sampleQuery, results, error ) -> Void in

            if let queryError = error {
                print( "There was an error while reading the samples: \(queryError.localizedDescription)")
            }

            workouts = results as! [HKWorkout]

            let target:Int = 0
            print(workouts[target].workoutEvents)
            print("Energy ", workouts[target].totalEnergyBurned)
            print(durationFormatter.stringFromTimeInterval(workouts[target].duration))
            print((workouts[target].totalDistance!.doubleValueForUnit(HKUnit.meterUnit())))

            self.coolMan(workouts[target])
            self.coolManStat(workouts[target])
    }

    // Execute the query
    healthStore.executeQuery(sampleQuery)
}

func coolMan(let workout: HKWorkout){

    let expectedOutput = [
        NSTimeInterval(293),
        NSTimeInterval(359),
        NSTimeInterval(359),
        NSTimeInterval(411),
        NSTimeInterval(810)
    ]

    let healthStore:HKHealthStore = HKHealthStore()

    let distanceType        = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)
    let workoutPredicate    = HKQuery.predicateForObjectsFromWorkout(workout)
    let startDateSort       = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)

    let query               = HKSampleQuery(sampleType: distanceType!, predicate: workoutPredicate,
        limit: 0, sortDescriptors: [startDateSort]) {
            (sampleQuery, results, error) -> Void in

            // Process the detailed samples...
            if let distanceSamples = results as? [HKQuantitySample] {

                var count = 0.00, countPace = 0.00, countDistance = 0.0, countPacePerMeterSum = 0.0
                var countSplits = 0
                var firstStart = distanceSamples[0].startDate
                let durationFormatter = NSDateComponentsFormatter()

                print("🕒 Time Splits: ")
                for (index, element) in distanceSamples.enumerate() {
                    count +=  element.quantity.doubleValueForUnit(HKUnit.meterUnit())

                    /* Calculate Pace */
                    let duration = ((element.endDate.timeIntervalSinceDate(element.startDate)))
                    let distance = distanceSamples[index].quantity
                    let pacePerMeter = distance.doubleValueForUnit(HKUnit.meterUnit()) / duration

                    countPace += duration
                    countPacePerMeterSum += pacePerMeter

                    if count > 1000 {


                        /* Account for extra bits */
                        let percentageUnder = (1000 / count)
                        //countPace = countPace * percentageUnder
                        // 6.83299013038 * 2.5
                        print("👣 Reached Kilometer \(count) ")

                        // MARK: Testing
                        let testOutput          = durationFormatter.stringFromTimeInterval(NSTimeInterval.init(floatLiteral: test)),
                            testOutputExpected  = durationFormatter.stringFromTimeInterval(expectedOutput[countSplits])

                        print("   Output Accuracy (", round(test - expectedOutput[countSplits]) , "): expected \(testOutputExpected) versus \(testOutput)")
                        print("   ", firstStart, " until ", element.endDate)

                        /* Print The Split Time Taken */
                        firstStart = distanceSamples[index].endDate;
                        count = (count % 1000) //0.00
                        countPace = (count % 1000) * pacePerMeter
                        countSplits++

                        /* Noise 
                        \(countSplits) – \(count) – Pace \(countPace) – Pace Per Meter \(pacePerMeter) – Summed Pace Per Meter \(countPacePerMeterSum) – \(countPacePerMeterSum / Double.init(index))"
                        */
                    }

                    /* Account for the last entry */
                    if (distanceSamples.count - 1 ) == index {
                        print("We started a kilometer \(countSplits+1) – \(count)")
                        let pacePerKM = (count / countPace) * 1000
                        print(durationFormatter.stringFromTimeInterval(NSTimeInterval.init(floatLiteral: (pacePerKM ))))
                    }
                }

            }else {
                // Perform proper error handling here...
                print("*** An error occurred while adding a sample to " + "the workout: \(error!.localizedDescription)")
                abort()
            }
    }
    healthStore.executeQuery(query)
}

func coolManStat(let workout: HKWorkout){

    let healthStore:HKHealthStore = HKHealthStore()

    let stepsCount = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceWalkingRunning)


    let sumOption = HKStatisticsOptions.CumulativeSum

    let statisticsSumQuery = HKStatisticsQuery(quantityType: stepsCount!, quantitySamplePredicate: HKQuery.predicateForObjectsFromWorkout(workout),
        options: sumOption)
        {  (query, result, error) in
            if let sumQuantity = result?.sumQuantity() {
                let numberOfSteps = Int(sumQuantity.doubleValueForUnit(HKUnit.meterUnit()))/1000
                print("👣 Right -O: ",numberOfSteps)


            }


    }

    healthStore.executeQuery(statisticsSumQuery)

}

Actual Output Output Log


Solution

  • I'm sure you're past this problem by now, more than two years later! But I'm sure someone else will come across this thread in the future, so I thought I'd share the answer.

    I started off with a version of your code (many thanks!!) and encountered the same problems. I had to make a few changes. Not all of those changes are related to the issues you were seeing, but in any case, here are all of the considerations I've thought of so far:

    • Drift

      You don't handle the 'drift', although this isn't what's causing the big inaccuracies in your output. What I mean is that your code is saying:

      if count > 1000

      But you don't do anything with the remainder over 1000, so your kilometre time isn't for 1000m, it's for, let's say, 1001m. So your time both is inaccurate for the current km, and it's including some of the running from the next km, so that time will be wrong too. Over a long run, this could start to cause noticeable problems. But it's not a big deal over short runs as I don't think the difference is significant enough at small distances. But it's definitely worth fixing. In my code I'm assuming that the runner was moving at a constant pace during the current sample (which is obviously not perfect, but I don't think there's a better way), and I'm then simply finding the fraction of the current sample distance that puts the split distance over 1000m, and getting that same fraction of the current sample's duration and removing it from the current km's time, and adding it (and the distance) to the next split.

    • GPS drops

      The real problem with your results is that you don't handle GPS drops. The way I'm currently handling this is to compare the startDate of the current sample with the endDate of the previous sample. If they're not the same then there was a GPS drop. You need to add the difference between the previous endDate and the current startDate to the current split. Edit: you also need to do this with the startDate of the activity and the startDate of the first sample. There will be a gap between these 2 dates while GPS was connecting.

    • Pauses

      There's a slight complication to the above GPS dropping problem. If the user has paused the workout then there will also be a difference between the current sample's startDate and the previous sample's endDate. So you need to be able to detect that and not adjust the split in that case. However, if the user's GPS dropped and they also paused during that time then you'll need to subtract the pause time from the missing time before adding it to the split.

    Unfortunately, my splits are still not 100% in sync with the Apple Workouts app. But they've gone from being potentially minutes off to being mostly within 1 second. The worst I've seen is 3 seconds. I've only been working on this for a couple of hours, so I plan to continue trying to get 100% accuracy. I'll update this answer if I get that. But I believe I've covered the major problems here.