I want to update the Published property of my ChallengeManager class with data passed in from LocationManager. Here is the simplified code with the relevant bits:

LocationManager

final class LocationManager: NSObject, ObservableObject {
    var challengeManager = ChallengeManager()
    ...
    //a func called from locationManager delegate converts the region to an instance of an Area object then calls a method on the ChallengeManager class like this:
    
    challengeManager.loadChallenge(for: activeArea)
...

ChallengeManager

final class ChallengeManager: ObservableObject {
   @Published var isShowingChallenge = false
   @Published var challengeToDisplay: Challenge?

func loadChallenge(for area: Area) {
   if let challenge = area.challenge { //gets challenge property of area object
      self.challengeToDisplay = challenge
      self.isShowingChallenge = true
   }
}

Finally, the ContentView:

struct ContentView: View {
   @ObservedObject var challengeManager = ChallengeManager()
...

(To be honest, I can get the results I want by adding an ObservedObject for the LocationManager in the View and then passing the values into a func there. But I don't like the idea of doing this for multiple views. And I also want loadChallenge() to do more heavy lifting. It seems to me that it should be the single source of truth. No?)

The problem:

If I try to access challengeManager.challengeToDisplay inside the ContentView, the value is always nil. Print statements in the loadChallenge() func tell me that the value from the locationManager is being received correctly. But @Published var challengeToDisplay is not changing. Can someone please tell me what I am doing wrong?
Thanks!

2

There are 2 best solutions below

1
On
@ObservedObject var challengeManager = ChallengeManager()

This is creating a new instance of ChallengeManager, which won't be the one you're dealing with inside LocationManager.

You should be passing in the challenge manager when instantiating ContentView, not giving the property a default value.

2
On

challengeManager in LocationManager and challengeManager in ContentView are two different instances. They are not related. You have to use the same instance.

The convention is

  • @StateObject creates an object and owns it

    @StateObject var challengeManager = ChallengeManager()
    
  • @ObservedObject does not create and own the object, it will be passed thru the view hierarchy.

    @ObservedObject var challengeManager : ChallengeManager
    

You can also use @EnvironmentObject somewhere at the beginning of the view hierarchy.

Side note:

SwiftUI relies on non-optionals much more than Swift. For the published challenge this enum with associated types if more SwiftUI-like

enum ChallengeState {
    case idle, display(Challenge)
}

and

final class ChallengeManager: ObservableObject {
   @Published var challengeState : ChallengeState = .idle

func loadChallenge(for area: Area) {
   if let challenge = area.challenge { //gets challenge property of area object
      challengeState = .display(challenge)
   } else {
      challengeState = .idle
   }
}

In the view switch on the state.