0

I'm making a CrossFit app that has, as one of the features, a stopwatch or timer that the user will start and stop, and when it's stopped, it'll log the exercise to Health Kit, it will calculate the approximate calories based on the exercise intensity and time to complete the exercise, and will finally show a sheet telling the user the number of calories consumed and how much time it took.

The view that contains the stopwatch and the buttons is the following:

struct FooterTimerView: View {
    @State private var completedSheetPresented = false
    var workout: Workout? = nil
    var subworkout: Subworkout? = nil
    @Environment(\.managedObjectContext) private var viewContext
    @EnvironmentObject var healthViewModel: HealthViewModel
    
    fileprivate func getConsumedCaloriesAndShowSheet(_ completable: Completable, timeInMinutes minutes: Int) async -> Double {
        return await healthViewModel.logCompletable(completable, timeInMinutes: minutes, inContext: viewContext) ?? 0.0
    }
    
    var body: some View {
        var loggedTime = ("", "", "")
        var consumedCalories = 0.0
        return TimerView { (timeString: String) in
            loggedTime = converTimeStringToTimeInterval(timeString)
            if let minutes = Int(loggedTime.0), let seconds = Int(loggedTime.1), let miliseconds = Int(loggedTime.2) {
                let totalMinutes = minutes + seconds / 60 + (miliseconds / (60 * 1000))
                if let workout = workout {
                    Task {
                        consumedCalories = await getConsumedCaloriesAndShowSheet(workout, timeInMinutes: totalMinutes)
                        completedSheetPresented = true
                    }
                } else if let subworkout = subworkout {
                    Task {
                        consumedCalories = await getConsumedCaloriesAndShowSheet(subworkout, timeInMinutes: totalMinutes)
                        completedSheetPresented = true
                    }
                }
            }
        }.sheet(isPresented: $completedSheetPresented) {
            CompletedWorkoutSheet(workoutTime: loggedTime, workoutCalories: consumedCalories)
        }
    }
    
    private func converTimeStringToTimeInterval(_ timeString: String) -> (String, String, String) {
        let timeString = "59:56:23"
        let components = timeString.components(separatedBy: ":")
        let millis = components[2]
        let seconds = components[1]
        let minutes = components[0]
        return (minutes, seconds, millis)
    }
}

To notice in the code, the two variables that the sheet will need (loggedTime and consumedCalories) are defined in the body of the view, so the sheet can use their values, and I'm not sure if there's a better way to do this.

Also, my view TimerView which is created in the code above has the following code:

struct TimerView: View {
    let stopWatchFrameHeight = CGFloat(70)
    let stopWatchFontSize = CGFloat(70)
    
    @StateObject var stopWatch = StopWatch()
    let onTimerFinished: (String) -> Void
    
    init(onTimerFinishedAction: @escaping (String) -> Void) {
        self.onTimerFinished = onTimerFinishedAction
    }

so when it is called in the FooterTimerView, the lambda passed to it is a function to be executed when the timer is finished - which is when the consumed calories can be calculated based on the time spent exercising.

The problem I'm facing is that in the two assignments to the consumedCalories variable, I get the error Mutation of captured var 'consumedCalories' in concurrently-executing code and I'm not sure how to fix the code so that:

  1. The amount of calories is calculated first in an async way.
  2. Once the 1. is finished, the sheet is shown, and it takes the consumedCalories value to it is shown to the user.

I've been trying to fix it for a couple of days and as soon as I think I'm heading in the right direction and I start fixing it, I always end up at a dead end.

Thanks a lot in advance!

noloman
  • 11,411
  • 20
  • 82
  • 129
  • 1
    The main goal of Swift Concurrency is to avoid data races and modifying a *captured var 'consumedCalories' in concurrently-executing code* can cause a data race. You should remove all the calculation code from the view, put it one of the (view) models and `@publish` the result. – vadian Apr 18 '22 at 13:50
  • thanks a lot @vadian it indeed fixed my issue. Sometimes I really miss where the VM and its `@Published` fit in all this async/await concurrency. – noloman Apr 22 '22 at 22:31

0 Answers0