Search code examples
iosswiftuiwatchkithealthkit

HealthKit - Change HKWorkoutConfiguration during HKWorkoutSession?


My question is the following:

Can I change the HKWorkoutConfiguration.activityType during a HKWorkoutSession or does each HKWorkoutSession has to have its own HKWorkoutConfiguration.activityType?

I want to create a workout app where you can create a workout consisting of different sets with different activity types. For example a Shadowing Boxing Workout, consisting of 3 sets of Boxing and 3 sets of Kickboxing (Boxing and Kickboxing are the different activities).

Ideally I would just start the HKWorkoutSession once at the beginning and end it after all sets for each activity are done, changing the HKWorkoutConfiguration.activityType in-between.

My current approach is based on the sample provided by Apple: https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings/speedysloth_creating_a_workout

I adjusted the startWorkout() method to startWorkout(for type: String). It now looks like this:

// Start the workout.
    func startWorkout(for type: String) {
        // Start the timer.
        setUpTimer()
        self.running = true
        
        // Create the session and obtain the workout builder.
        /// - Tag: CreateWorkout
        do {
            session = try HKWorkoutSession(healthStore: healthStore, configuration: self.workoutConfiguration(for: type))
            
            builder = session.associatedWorkoutBuilder()
        } catch {
            // Handle any exceptions.
            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: workoutConfiguration(for: type))
        
        // Start the workout session and begin data collection.
        /// - Tag: StartSession
        session.startActivity(with: Date())
        builder.beginCollection(withStart: Date()) { (success, error) in
            // The workout has started.
        }
        print("New Workout has started")
    }

In the method I get the respective activity by workoutConfiguration(for: type) which looks up the right activity from a string.

After a set is done (e.g. the boxing set), I end the session and start a new workout and session for the new set with the new activity.

My problem with the current approach is that I need to end the current HKWorkoutSession before I start the new one. But ending the session the way its done in the example does not execute immediately and therefore the new set of the workouts starts without saving the old set to the HKStore with the right activity.

Therefore I thought I would be nice to start the session just once and switch activityTypes in-between. However, I don't know if it is possible (maybe complications with HKStore) and how it is done.

Or is there any other smart way of doing things to achieve this?

I'm just starting out with iOS Programming.

Any help is greatly appreciated!


Solution

  • Disclaimer: This isn't a full answer, but an approach I'd take to address the problem.

    Look at the workoutSession(_:didChangeTo:from:date:) method. Documentation Link

    When one type of workout ends that method will receive a notification. As long as you haven't called session.end() the workout session should still be active. Use your healthStore instance to save the previous workout session before starting a new one.

    It could look like this;

    func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) {
        if toState == .ended {
            workoutBuilder.endCollection(withEnd: date) { (success, error) in
                // collection has ended for one type of workout
                self.workoutBuilder.finishWorkout { (workout, error) in
                    // unwrap the optional `workout`
                    guard let completedWorkout = workout else { return }
                    // save workout to health store
                    healthStore.save(completedWorkout) { (success, error) in
                        if success {
                            // begin a new workout by calling your method to start a new workout with a new configuration here
                            startWorkout(for type: String)
                        }
                    }
                }
            }
        }
    }
    

    That is likely going to cause issues with your Timer since you're calling setUpTimer again but you can work with that. Also this doesn't address the point when the user is completely done with their workout and doesn't want to start a new workout type.