The @ObservableObject and @Published property wrappers are the sauce of Combine powered apps. With Combine and SwiftUI, it’s easy to use the @Published wrapper in our ViewModel properties and have the Views automatically update as changes to these happen.
Everything works great until you want to use Protocols to facilitate dependency injection and testing in your Models and ViewModel classes, as we’ve been doing in our regular MVVM apps for the past few years. You will soon discover that Swift (As of now, version 5.3) does not support property wrappers in Protocol declarations, and marking a property as @Published in a protocol will throw an error.
To explain the issue, we will use a Playground to write a quick demo app using Combine and SwiftUI, following the MVVM pattern. It will consist of three components. (You guessed: Model, ViewModel and View)
Here’s the Code:
And Voilá!, Here’s the Result:
This is our Model. It holds a list of animal names and has a method to publish one random animal name via a Publisher. (@Published private(set) var name)
Our ViewModel, responsible for managing the View. It owns our Model, subscribes to it and then re-publishes name via its displayData publisher. (@Published var textToShow).
For the sake of simplicity we are using a SwiftUI View with a text and a button. It owns our ViewModel and subscribes to it. It displays the ViewModel’s displayData value as a Text whenever the data changes.
When you tap the button, it calls the generate() method in the ViewModel. The ViewModel calls the same method in our Model and Combine takes cares of publishing the changes.
And Where is the Protocol?
Everything works so far, but what if instead of instantiating AnimalGenerator in our ViewModel, we want to use a Protocol as a blueprint to different models with different data generators. This is when Protocol Oriented Programming shines.
Let’s write a simple protocol called “Generator” and update our “AnimalGenerator” class to conform.
Our AnimalGenerator Model conforming to it
Houston, we have a problem
The error is reminding us what I was mentioning at the beginning: Wrappers and Stored properties are not allowed in Swift protocols and extensions (at least for now).
Before doing the fix, let’s talk about the @Published wrapper.
In essence, a property wrapper is a type that wraps a value and attaches some logic to it. When a property has a @Published wrapper, some invisible logic is attached to it in order to automatically publish its value anytime it changes. In this case, it happens explicitly during the willSet block execution..
In simple terms, @Published creates a publisher that can be accessed with the ’$’operator (as we do in our ViewModel), allowing any subscriber to get its value whenever it changes in the future.
Since we cannot use a @Published wrapper as part of our protocol declaration, we need to describe its synthesized methods explicitly. Let’s add them to our Generator protocol and AnimalGenerator Model.
Our Updated Protocol
Our Updated Model conforming to it
We have now manually defined our Publisher in the Protocol declaration (namePublisher), and exposed both the Publisher (namePublisher) and the Published property (namePubished) in the Model. Now let’s update our ViewModel to make it work.
Our Updated ViewModel
Note that we are now using our manually exposed generator.namePublisher and we are not using the ’$’ operator anymore with generator.$name
And now, let’s fix the View
In the original example, we are instantiating the ViewModel from inside the View, and we can continue to do the same, just by passing the Generator we want to use, like this:
Our Updated View
It works, but it does not look good, as we are now mashing the View, ViewModel and Model together there. Let’s better instantiate our ViewModel and Model separately and inject them into our View.
First, let’s update our View and remove the initialization for the ViewModel.
The final version
And then, we can simply instantiate our Model and ViewModel and pass them to the View like this.
Instantiating and injecting our ViewModel
Let’s try it out with another Model
Note on iOS 14
iOS 14 introduced a new @StateObject wrapper, and it might be a good idea to use it instead of @ObservedObject.
An @ObservedObject instance is replaced every time SwiftUI decides to discard and redraw the View, and thus you might experience weird crashes in different scenarios. By using @StateObject, you ensure the instance we create is retained even when the View is redrawn or discarded.
Using @StateObject is then particularly useful when you are instantiating your ViewModel from the View itself, as we did in the first example
As you can see, it is reasonably straightforward to use Combine and still maintain polymorphic interfaces via Protocol Oriented Programming in your classes.
Some would say that the next step would be decoupling the View itself to become completely ignorant of it’s ViewModel and use a Protocols there too. It can be done easily if your are using UIKit, although it will require a little more work with SwiftUI. Nevertheless, for this case, It’s overkill. Maybe we can cover that in another post.
I hope you have enjoyed this article. Feel free to follow me and reach out on Twitter if you have any questions or feedback.