Home > front end >  Passing data between @StateObjects in SwiftUI
Passing data between @StateObjects in SwiftUI

Time:12-19

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

CodePudding user response:

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

CodePudding user response:

@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)
        }
    }
}
  • Related