Delay on Lap events from HKLiveWorkoutBuilder workoutBuilderDidCollectEvent

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
            }
        }
    }
    
}

Replies

Same here on Apple Watch

Results are delayed and being displayed only after 2 laps

Did you find any solutions?