I'm having an issue with a Simple HealthKit swimming Demo on an Apple Watch I'm working on. I'm integrating with the HKLiveWorkoutBuilder and trying to simply count the number of laps a user is swimming on an indoor pool.
I get up to date data from
workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>)
. However, the workoutBuilderDidCollectEvent
for the lap datatype is coming in delayed by 20+ seconds, resulting in really inaccurate data. Has anyone else run into something like this with HealthKit before?For some more detail. I receive below lap event at
2020-01-23 19:52:32 +0000
(lldb) po workoutBuilder.workoutEvents.last
▿ Optional
- some : HKWorkoutEventTypeLap, <_NSConcreteDateInterval: 0x1569aa60> (Start Date) 2020-01-23 19:51:47 +0000 + (Duration) 22.497641 seconds = (End Date) 2020-01-23 19:52:09 +0000 {
HKSwimmingStrokeStyle = 2;
}
Modified SpeedSloth WorkoutSession class which demonstrates the delay:
/*
See LICENSE folder for this sample’s licensing information.
Abstract:
THe workout session interface controller.
*/
import WatchKit
import Foundation
import HealthKit
class WorkoutSession: WKInterfaceController, HKWorkoutSessionDelegate, HKLiveWorkoutBuilderDelegate {
@IBOutlet weak var timer: WKInterfaceTimer!
@IBOutlet weak var activeCaloriesLabel: WKInterfaceLabel!
@IBOutlet weak var heartRateLabel: WKInterfaceLabel!
@IBOutlet weak var distanceLabel: WKInterfaceLabel!
var healthStore: HKHealthStore!
var configuration: HKWorkoutConfiguration!
var session: HKWorkoutSession!
var builder: HKLiveWorkoutBuilder!
var laps = 0
override func awake(withContext context: Any?) {
super.awake(withContext: context)
setupWorkoutSessionInterface(with: context)
// Create the session and obtain the workout builder.
/// - Tag: CreateWorkout
do {
session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
builder = session.associatedWorkoutBuilder()
} catch {
dismiss()
return
}
// Setup session and builder.
session.delegate = self
builder.delegate = self
/// Set the workout builder's data source.
/// - Tag: SetDataSource
builder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore,
workoutConfiguration: configuration)
// Start the workout session and begin data collection.
/// - Tag: StartSession
session.startActivity(with: Date())
builder.beginCollection(withStart: Date()) { (success, error) in
self.setDurationTimerDate(.running)
}
}
// Track elapsed time.
func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
// Retreive the workout event.
guard let workoutEventType = workoutBuilder.workoutEvents.last?.type else { return }
// Update the timer based on the event received.
switch workoutEventType {
case .pause: // The user paused the workout.
setDurationTimerDate(.paused)
case .resume: // The user resumed the workout.
setDurationTimerDate(.running)
case .lap:
WKInterfaceDevice.current().play(.notification)
print(Date())
print("Lap");
default:
return
}
}
func setDurationTimerDate(_ sessionState: HKWorkoutSessionState) {
/// Obtain the elapsed time from the workout builder.
/// - Tag: ObtainElapsedTime
let timerDate = Date(timeInterval: -self.builder.elapsedTime, since: Date())
// Dispatch to main, because we are updating the interface.
DispatchQueue.main.async {
self.timer.setDate(timerDate)
}
// Dispatch to main, because we are updating the interface.
DispatchQueue.main.async {
/// Update the timer based on the state we are in.
/// - Tag: UpdateTimer
sessionState == .running ? self.timer.start() : self.timer.stop()
}
}
// MARK: - HKLiveWorkoutBuilderDelegate
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else {
return // Nothing to do.
}
if(quantityType == HKQuantityType.quantityType(forIdentifier: .distanceSwimming)){
WKInterfaceDevice.current().play(.success)
}
/// - Tag: GetStatistics
let statistics = workoutBuilder.statistics(for: quantityType)
let label = labelForQuantityType(quantityType)
updateLabel(label, withStatistics: statistics)
}
}
// MARK: - State Control
func pauseWorkout() {
session.pause()
}
func resumeWorkout() {
session.resume()
}
func endWorkout() {
/// Update the timer based on the state we are in.
/// - Tag: SaveWorkout
session.end()
builder.endCollection(withEnd: Date()) { (success, error) in
self.builder.finishWorkout { (workout, error) in
// Dispatch to main, because we are updating the interface.
DispatchQueue.main.async() {
self.dismiss()
}
}
}
}
func setupWorkoutSessionInterface(with context: Any?) {
guard let context = context as? WorkoutSessionContext else {
dismiss()
return
}
healthStore = context.healthStore
configuration = context.configuration
setupMenuItemsForWorkoutSessionState(.running)
}
/// Set up the contextual menu based on the workout session state.
func setupMenuItemsForWorkoutSessionState(_ state: HKWorkoutSessionState) {
clearAllMenuItems()
if state == .running {
addMenuItem(with: .pause, title: "Pause", action: #selector(pauseWorkoutAction))
} else if state == .paused {
addMenuItem(with: .resume, title: "Resume", action: #selector(resumeWorkoutAction))
}
addMenuItem(with: .decline, title: "End", action: #selector(endWorkoutAction))
}
/// Action for the "Pause" menu item.
@objc
func pauseWorkoutAction() {
pauseWorkout()
}
/// Action for the "Resume" menu item.
@objc
func resumeWorkoutAction() {
resumeWorkout()
}
/// Action for the "End" menu item.
@objc
func endWorkoutAction() {
endWorkout()
}
// MARK: - HKWorkoutSessionDelegate
func workoutSession(
_ workoutSession: HKWorkoutSession,
didChangeTo toState: HKWorkoutSessionState,
from fromState: HKWorkoutSessionState,
date: Date
) {
// Dispatch to main, because we are updating the interface.
DispatchQueue.main.async {
self.setupMenuItemsForWorkoutSessionState(toState)
}
}
func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
print("Error")
}
// MARK: - Update the interface
/// Retreive the WKInterfaceLabel object for the quantity types we are observing.
func labelForQuantityType(_ type: HKQuantityType) -> WKInterfaceLabel? {
switch type {
case HKQuantityType.quantityType(forIdentifier: .heartRate):
return heartRateLabel
case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned):
return activeCaloriesLabel
case HKQuantityType.quantityType(forIdentifier: .distanceSwimming):
return distanceLabel
default:
return nil
}
}
/// Update the WKInterfaceLabels with new data.
func updateLabel(_ label: WKInterfaceLabel?, withStatistics statistics: HKStatistics?) {
// Make sure we got non `nil` parameters.
guard let label = label, let statistics = statistics else {
return
}
// Dispatch to main, because we are updating the interface.
DispatchQueue.main.async {
switch statistics.quantityType {
case HKQuantityType.quantityType(forIdentifier: .heartRate):
/// - Tag: SetLabel
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
let value = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
label.setText("\(roundedValue) BPM")
case HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned):
let energyUnit = HKUnit.kilocalorie()
let value = statistics.sumQuantity()?.doubleValue(for: energyUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
label.setText("\(roundedValue) cal")
return
case HKQuantityType.quantityType(forIdentifier: .distanceSwimming):
let meterUnit = HKUnit.yard()
let value = statistics.sumQuantity()?.doubleValue(for: meterUnit)
let roundedValue = Double( round( 1 * value! ) / 1 )
label.setText("\(roundedValue) yrd")
return
default:
return
}
}
}
}