I have a singleton data manager class with @Published
high level models. I need to process these through a viewmodel class pass the simplified display models / properties back to the view. For the first time loading this works, but the changes on data manager aren't observed on the SwiftUI View.
class DataManager: ObservableObject {
static let shared = DataManaer()
@Published var modelsA: [ModelA]
}
struct ViewA: View {
@StateObject var vm = ViewModel()
var body: some View {
// UI Code Like similar to this
ForEach(vm.displayModels) {
ItemView($0)
}
}
}
class ViewModel: ObservableObject {
// Few other properties here
@Published var someState: Bool
var dataManager = DataManager.shared
// This is a computed property based on datamanager's models
var displayModels: [DisplayModel] {
dataManager.modelsA.compactMap { MapModelAToDisplayModel($0) }
}
}
Can anyone shed some light on how to fix the above code, so that when DataManager's modelsA or modelsB changes, the computed property from ViewModels would trigger view update?
There are two reasons:
You could use Combine to listen to changes of that published property and then update your own property, which you then mark as @Published. Like so:
import Combine
class ViewModel: ObservableObject {
var dataManager = DataManager.shared
var cancellables = Set<AnyCancellable>()
@Published
var displayModels = [DisplayModel]()
init() {
dataManager.$modelsA
.receive(on: DispatchQueue.main)
.sink { [weak self] models in
self?.displayModels = models.compactMap { MapModelAToDisplayModel($0) }
}
.store(in: &cancellables)
}
}
That will explicitly listen to changes in modelsA
of DataManager
and will then do the mapping when a change got signalled.
There are a few important tidbits hidden in the code:
First of all, you need that one line .receive(on: DispatchQueue.main)
, because in SwiftUI it's important to only do changes to the UI (that includes @Published properties that influence UI) on the main thread.
Also it is important to have a [weak self]
reference in the sink. Otherwise you will end up with a retain cycle and leak memory. (ViewModel holds the DataManager, that holds the models publisher, that then holds a reference to the sink, which in turn holds a reference to the ViewModel.) Using [weak self]
fixes that issue :)