Observing a KVO compatible model in SwiftUI and MVVM
In model-view-view model (MVVM) architecture the view model observes the model and provides data for the view by transforming it when needed. When the user interacts with the view and changes the data in it then the view model’s responsibility is to propagate those mutations back to the model object. Therefore, the important part in MVVM is how to manage data flows between objects. This time we’ll take a look on observing key-value observing (KVO) compatible model objects with Combine framework in SwiftUI view. The example view what we’ll build looks like this:
The model object represents a package which contains information about the recipient, the sender, and the contents. The recipient and the sender are represented by a Person object which includes a first name, a last name, and a postal address. The contents is an array of immutable PackageContent objects. In Swift, we can use KVO by specifying @objc and dynamic modifiers on properties. Dynamic means that method dispatch is using objective-c runtime and therefore all the types must be representable in objective-c runtime. This immediately adds restrictions to the types we can use. When writing pure Swift code I do not recommend using KVO but sometimes we just need to use it. One example is NSManagedObject from the CoreData framework. But in this app we are not dealing with NSManagedObject but with a simple NSObject subclass instead.
The view object is responsible for describing the UI and rendering data represented by the view model. We have a simple form for modifying the recipient’s first name, last name, and the street name (for keeping this UI simple I left out all the other postal address related properties). At the bottom of the view we have a text object which just describes the package and a button for adding new items to the package’s contents. Whenever any of the package’s properties change, the view needs to reload. View reload is done through the @StateObject property wrapper (read mode about observing view models in MVVM in SwiftUI and @StateObject and MVVM in SwiftUI).
View Model layer
The view model’s responsibility is to observe the model object and propagating view changes to the model. It acts as a transformation layer where we can transform any data in the model to anything suitable for displaying. In the example below we are reading CNPostalAddress and only returning street name and reading multiple properties and returning a summary string. View models make it easy to contain such logic and also make it more easy to test.
Foundation framework defines a publisher named NSObject.KeyValueObservingPublisher which can be used for observing KVO compatible properties. One of the approaches is to use this publisher and then bind the model changes to the view model’s own property. Combine framework provides a convenient assign operator which takes a target publisher as an argument. Convenient because we can connect it with @Published properties in the view model. @Published properties automatically notify the ObservableObject’s objectWillChange publisher which is observed by a SwiftUI view. As soon as the property changes, SwiftUI view picks up the change and reloads itself. Note that we’ll also need to propagate changes back to the model when user updates the view and therfore the @Published property. This can be achieved by connecting property’s publisher with dropFirst, removeDuplicates and assign publishers where the latter assigns the value to the model object. Drop first is used for ignoring the initial value of the property. One downside is that now we can have the same information both in the view model and in the model. But on the other hand it makes the data streams easy to read and no need to have extra observation for triggering the view reload by manually calling send() on the objectWillChange publisher.
Another approach what we can use is providing properties in the view model which return a Binding. This allows us to write the transformation code inside the get and set closures. This is what we have done with the street property. Note that we’ll still need to observe the model as the model can change at any point. Binding just provides a way of accessing the value. Therefore, we’ll need to set up an observation and calling send() on the objectWillChange publisher.
If we go back to the SwiftUI view and connect all the properties then the full implementation of the view model looks like this:
Key-value observing is getting less and less used after the introduction of Combine and SwiftUI. But there are still times when we need to connect good old KVO compatible NSObject subclasses with a SwiftUI view. Therefore, it is good to know how to handle KVO in SwiftUI views as well.
SwiftUIMVVMKVOObserving (GitHub, Xcode 12.1)