-1

After having searched the documentation and reading this essentially unanswered question I still don't know how to pass a @Published var from Class A to Class B in SwiftUI.

I'm not sure if I have fundamentally misunderstood something, since in my opinion the following use-case is quite simple: Suppose we have two "ViewModels" or "Controllers"

  1. LocationManager
  2. PostManager

The LocationManager has a the current location, while the PostManager -completely independently- is responsible for fetching some posts which it then stores in a property "posts". Let's say this fetching is done based on location, so only posts that have a location in a specific relation to the current location are fetched. By having the two managers separate, all the logic (and source of truth) is neatly organised and not directly in any view. Suppose both these classes live as @StateObject in a view. How can the PostManager always have an up to date version of the location to fetch posts (for example periodically, without any input from any view)?

Of course, as a commenter in the linked question suggests, one could use UserDefaults to accomplish this, but I feel like that is rather a workaround than a state of the art implementation..

Example in code

import Foundation
import SwiftUI

class LocationManager: ObservableObject {
    
    @Published var location: [Double] = []
    
    init() {
        // running stuff to keep location up to date
    }
}

class PostManager: ObservableObject {
    
    @Published var posts: [LocationPost] = []
    
    init() {
        // fetching posts based on location in LocationManager instance <- but how do we always have this information
    }
}

class LocationPost {
    var text: String = ""
    var coordinates: [Double] = []
    
    convenience init(coordinates: [Double] = [], test: String? = nil) {
        self.init()
        self.coordinates = coordinates
        self.text = text
    }
}
D. Kee
  • 169
  • 14
  • If the view does not need the LocationManager, and just the PostManager requires it, consider moving it inside the PostManager. – MrAlirezaa Dec 17 '22 at 13:17
  • State and its reference counterpart StateObject are sources of truth you would never make one depend on another because then it is no longer a source of truth. – malhal Dec 18 '22 at 04:12
  • @malhal I tried to show with this example that even with this concept in mind it could still happen. The source of truth of the location is in one and the source of truth off posts in another. But where else would the logic run that needs access to both this data? To create posts that need a location in a view seems also the wrong place... – D. Kee Dec 18 '22 at 11:29
  • @MrAlirezaa could you elaborate, maybe as an answer? Do you mean just moving all the logic into one ObservableObject class? Either way I feel like then the sources of truth are not well separated, which also feels wrong. – D. Kee Dec 18 '22 at 11:35
  • @D.Kee you can't `pass a @Published var from Class A to Class B` you can subscribe to it with all of the examples provided by the two people that have answered and more. You might want to look into Dependency injection though, I think that is more of what you are looking for all those solutions are spaghetti like. – lorem ipsum Dec 18 '22 at 13:00
  • Have a "PostLocationManager" which publishes Posts and Locations AND does all the logic internally. Have views without doing any logic which subscribe to either or both and just render what they have been told. If you want to have more than one view subscribing to this Model, you should subcribe from a _shared_ ObservedObject. Note that "@State" and "@StateObject" are _private_ to a view, and such state objects can never directly communicate to each other or even to the outer "world" of the view. Views can send events/actions/intents to a Model, though. – CouchDeveloper Dec 18 '22 at 13:37
  • @D.Kee Check this answer https://stackoverflow.com/a/74841376/8249180. Example 2 is what I meant by the comment. But all other examples are correct too, in my opinion. – MrAlirezaa Dec 19 '22 at 09:29

2 Answers2

1

@Published Property Wrapper is actually wrapping a Combine Publisher on your property, and you could subscribe to it with to get updates when it changes.

To do this you have a few options and you could use the one that fits better your use case.

  • Pass LocationManager to PostManager so that it subscribes itself to changes, you can do this directly on the init, or in another method.
  • Pass PostManager to LocationManager.
  • Subscribe one to the other from a class or view holding or having a reference to them.
  • Etc.

Example 1

Subscribing from an external class or view

var cancellable: AnyCancellable? // hold a reference to it

cancellable = locationManager.$location.sink() { location in
    postManager.updateLocation(location)
}

// inside PostManager
func updateLocation(_ location: [Double]) {
    // location was updated
}

Example 2

Updating PostManager to receive LocationManager on the init

class PostManager: ObservableObject {
    
    @Published var posts: [LocationPost] = []
    private var cancellable: AnyCancellable?    

    init(locationManager: LocationManager) {
        cancellable = locationManager.$location.sink() { location in
            // location is updated
        }
    }
}

Example 3

If you have a View with both @StateObjects you can do it directly with SwiftUI.

struct MyView: View {
    @StateObject var locationManager = LocationManager()
    @StateObject var postManager = PostManager()
    
    var body: some View {
        VStack {
            
        }
        .onReceive(locationManager.$location) { location in
            postManager.updateLocation(location)
        }
    }
}
vicegax
  • 4,709
  • 28
  • 37
0

If the view has access to both ObservedObjects you can just pass the location from the view when creating new posts:

in PostManager:

func createPost(location: [Double], text: String) {
        posts.append(LocationPost(coordinates: location, text: text)) 
    }

in the view

@StateObject var locationmanager = LocationManager()
@StateObject var postmanager = PostManager()

...

   postmanager.create(locationmanager.location, text: "my Message")
ChrisR
  • 9,523
  • 1
  • 8
  • 26
  • Sorry for not being clear enough with the example. The answer you gave would solve the issue of creating the posts with the location at the time of posting. But my question is rather "how to pass a @Published var from Class A to Class B" in the sense that the data has to be **"live"**. I edited the question to give a better example to the problem, where your solution would unfortunately not work. – D. Kee Dec 18 '22 at 11:42