Search code examples
iosswiftswiftuicombinepublisher

How to use Combine's CurrentValueSubject and access it in a SwiftUI View?


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 })
    }
}

Solution

  • 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.