How to propagate Model changes through ViewModel in SwiftUI?

Viewed 1141

I am trying to understand the MVVM pattern in SwiftUI, but I am not understanding exactly how the ViewModel listens to and propagates changes from a Model object. Many examples, including this one from Apple, talk about having a Model inherit from ObservableObject and use that directly in your View. This makes sense.

But what is the best/recommended way that a ViewModel should observe changes to its underlying Model?

A simple example is a WidgetView that displays the title of a widget, but the title can change in the background from a background network call, for example.

class WidgetView: View {
    @ObservedObject var widgetVM = WidgetViewModel()

    var body: some View {
        Text(widgetVM.title)
    }
}

class WidgetViewModel : ObservableObject {
   var widget: Widget

   var title: String {
      get {
         // Some translation to the title for this particular view
         return widget.title + "!"
      }
   }
}

struct Widget {
    // Some other timer or background process is changing the title
    var title: String
}

One rough solution I've explored is having a separate title and listening for changes. So if Widget extended ObservableObject and @Published the title field, then the WidgetViewModel could do the following:

class WidgetViewModel : ObservableObject {
   var widget: Widget

   @Published var title: String = ""
   var cancellable: AnyCancellable?

   init() {
       self.cancellable = widget.$title.receive(on: DispatchQueue.main)
           .sink(receiveValue: self.updateTitle )
   }

   func updateTitle(_: String) {
       self.title = widget.title + "!"
   }
}

Is it recommended/standard for Widget to extend ObservableObject too? If so, how does WidgetViewModel properly pass notifications of changes through? It would seem that WidgetModelView.widget would need to be both a @Published and an @ObservedObject, but that doesn't seem right.

Anyone have any insight here?

1 Answers

Your Widget is struct, so it cannot be ObservableObject, instead you can just do the following

class WidgetViewModel : ObservableObject {

   var widget: Widget {
     didSet {
        updateTitle(widget)    // << here !!
     }
   }

   @Published var title: String = ""

   func updateTitle(_: String) {
       self.title = widget.title + "!"
   }
}
Related