skip to Main Content

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


  1. There are two reasons:

    1. You are using a computed property (meaning it it not @Published)
    2. Inside of the computed property, you directly read the values of the published property. You are not listening to that published property. (Meaning: you are only reading the models array of DataManager when the computed property is accessed directly.)

    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 🙂

    Login or Signup to reply.
  2. In SwiftUI the View struct is the view model already, so the solution is to remove the ViewModel class you made, e.g.

    
    struct ModelA: Identifiable {
    }
    
    class DataManager: ObservableObject {
      static let shared = DataManager()
      static let preview = DataManager(previewing: true)
    
      @Published var modelsA: [ModelA] = []
    
    //  func load()
    //  func save()
    }
    
    struct ViewA: View {
    
      @EnvironmentObject var dataManager
    
      var body: some View {
        // UI Code Like similar to this
        ForEach(dataManager.modelsA) { model in
            ItemView(model)
        }
      }
    
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search