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?
2
Answers
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:
That will explicitly listen to changes in
modelsA
ofDataManager
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 🙂In SwiftUI the
View
struct is the view model already, so the solution is to remove theViewModel
class you made, e.g.