Why is my HKWorkoutSession (usually) not ending?
Asked Answered
D

3

5

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.

Davide answered 22/7, 2020 at 19:35 Comment(0)
C
7

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) {
        
    }
}
Caiaphas answered 6/10, 2020 at 21:47 Comment(0)
P
2

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) {
        
    }
}
Phosphorite answered 12/2, 2021 at 18:36 Comment(0)
M
1

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)

Micrococcus answered 23/7, 2020 at 12:11 Comment(1)
thanks, but this doesn't quite end the workout. I'm curious how you read the error and can see that I'm trying to end the session twice? I'm terrible at reading error messages and debugging them.Davide

© 2022 - 2024 — McMap. All rights reserved.