3

My understanding is a CurrentValueSubject publisher in Combine is good for accessing on demand, as opposed to a regular Publisher that emits a value once. So I'm trying to use one here in an Environment Object to store the total energy burned in an HKWorkout so that I can access it after the workout is finished in a SwiftUI View. With the code below I get the compiler error Cannot convert return expression of type 'AnyCancellable' to return type 'Double' so I think I need to do some type of casting but can't figure it out?

class WorkoutManager: NSObject, HKWorkoutSessionDelegate, HKLiveWorkoutBuilderDelegate, ObservableObject  {
    
    var finishedWorkoutTotalEnergyBurned = CurrentValueSubject<Double, Never>(0.0)
    
    func stopWorkout() {
        self.finishedWorkoutTotalEnergyBurned.value = unwrappedWorkout.totalEnergyBurned!.doubleValue(for: .kilocalorie())
    }
}

struct SummaryView: View {

    @StateObject var workoutManager = WorkoutManager()
    
    var body: some View {
        Text("\(getFinishedWorkoutTotalEnergyBurned())")
            .navigationBarHidden(true)
        //.navigationTitle("Workout Complete")
    }
    
    func getFinishedWorkoutTotalEnergyBurned() -> Double {
        workoutManager.finishedWorkoutTotalEnergyBurned.sink(receiveValue: { $0 })
    }
}
New Dev
  • 48,427
  • 12
  • 87
  • 129
GarySabo
  • 5,806
  • 5
  • 49
  • 124

1 Answers1

1

I think your question shows that you need to learn a bit more the foundational principle behind SwiftUI, and behind Combine (that statement about "CurrentValueSubject vs regular Publisher" was quite wrong).

All you need here is to expose a @Published property on your WorkoutManager, and set it to the value that you want, when needed:

class WorkoutManager: NSObject, HKWorkoutSessionDelegate, HKLiveWorkoutBuilderDelegate, ObservableObject  {
    
   @Published var finishedWorkoutTotalEnergyBurned = 0.0
    
   func stopWorkout() {
      finishedWorkoutTotalEnergyBurned = unwrappedWorkout.totalEnergyBurned!.doubleValue(for: .kilocalorie())
   }
}
struct SummaryView: View {

  @StateObject var workoutManager = WorkoutManager()
    
  var body: some View {
     Text("\(workoutManager.finishedWorkoutTotalEnergyBurned)")
        .navigationBarHidden(true)
  }
}

@Published does use a Combine publisher under the hood, which in combination with @StateObject cause the view to update. But all you need to reason about here, is how and when to set these properties - and the view will update automatically. In the case you showed, there's likely no need to use a publisher directly.

New Dev
  • 48,427
  • 12
  • 87
  • 129
  • What I should have said as well is that the workout actually finishes in the view before this. That is what I meant about a publisher firing once (before SummaryView is created), so I need to be able to store the finishedWorkoutTotalEnergyBurned so that I can access it after finishing the workout and navigating to the SummeryView, which is why I thought CurrentValueSubject might work. – GarySabo Jul 22 '20 at 13:03
  • If it finishes, store the value somewhere (e.g. view model), and display it directly - no need for publishers – New Dev Jul 22 '20 at 13:05
  • Right my workout manager I thought was my view model so I was trying to store it there and read off of it – GarySabo Jul 22 '20 at 13:10
  • @GarySabo - that sounds like the right overall approach. It's hard to know from your question what the life cycle of those objects are, so it also sounds like maybe `WorkoutManager` isn't "owned" by `SummaryView`, so it being a `StateObject` of `SummaryView` isn't the right approach. Regardless, don't jump to Combine publishers until you are certain that this is what you need, and based on your usage so far, it isn't – New Dev Jul 22 '20 at 13:17
  • 2
    I figured it out and you were right, SummaryView wasn't getting passed the instance of the WorkoutManager with the data. I changed from @StateObject to `@EnvironmentObject private var workoutManager: WorkoutManager` and set it in the parent view via `SummaryView().environmentObject(workoutManager)` and I am now able to read the data off the workout manager. – GarySabo Jul 22 '20 at 16:34