Why is my HKWorkoutSession (usually) not ending?

Viewed 1097

I have a very simple workout app I'm working on for Apple Watch. It uses Health Kit to start and end workouts, my only problem is that when I try to end the workout it usually doesn't end the session and I get this error

2020-07-22 12:27:46.547720-0700 5k WatchKit Extension[25774:944527] [workouts] <HKWorkoutSession:0x80156310 A54AF52C-8B08-4BAD-A28C-03D8E54044B5 ended>: Failed to end: Error Domain=com.apple.healthkit Code=3 "Unable to transition to the desired state from the Ended(3) state (event 6). Allowed transitions from the current state are: {
    7 = "<error(7): Ended(3) -> Ended(3)>";
}" UserInfo={NSLocalizedDescription=Unable to transition to the desired state from the Ended(3) state (event 6). Allowed transitions from the current state are: {
    7 = "<error(7): Ended(3) -> Ended(3)>";
}}

I'm really not even sure where to start looking for the problem from this information, nor do I know what it means. Also, usually on the 4th or 5th try, it will actually end the workout session.

3 Answers

The probable cause is the wrong sequence of ending workout session and workout builder data collection.

If your code looks something simmilar to this, you'll get the error:

session.end()
        
builder.endCollection(withEnd: Date()) { (success, error) in
 builder.finishWorkout { (workout, error) in
// do something
 }
}

The session hasn't finished yet, but the code immediately is trying to end the builder's session.

In Apple's example (https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings/speedysloth_creating_a_workout) is the correct way of completing a workout through delegates:

session.delegate = self
.....
func endWorkout() {
        // End the workout session.
        session.end()
}
....
extension WorkoutManager: HKWorkoutSessionDelegate {
    func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState,
                        from fromState: HKWorkoutSessionState, date: Date) {
        // Wait for the session to transition states before ending the builder.
        /// - Tag: SaveWorkout
        if toState == .ended {
            print("The workout has now ended.")
            builder.endCollection(withEnd: Date()) { (success, error) in
                self.builder.finishWorkout { (workout, error) in
                    // Optionally display a workout summary to the user.
                    self.resetWorkout()
                }
            }
        }
    }
    
    func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
        
    }
}

Looks like you try to end session twice (when it's in ended state already).

Try to check the current state before ending.

if session.state == .running {
    session.stopActivity(with: Date())
}

if session.state == .stopped {
    session.end()
}

BTW from my experience it can take some time (some seconds) before green light turns off (You end session, and green light on your watch turns off some seconds later)

Building from @Nastya's answer, it was also necessary for me to set my session to nil:

session.delegate = self
.....
func endWorkout() {
        // End the workout session.
        session.end()
}
....
extension WorkoutManager: HKWorkoutSessionDelegate {
    func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState,
                        from fromState: HKWorkoutSessionState, date: Date) {
        // Wait for the session to transition states before ending the builder.
        /// - Tag: SaveWorkout
        if toState == .ended {
            print("The workout has now ended.")
            builder.endCollection(withEnd: Date()) { (success, error) in
                self.builder.finishWorkout { (workout, error) in
                    // Optionally display a workout summary to the user.
                    self.resetWorkout()
                    
                    // I had to add this step
                    self.session = nil
                }
            }
        }
    }
    
    func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {
        
    }
}
Related