I’m Only Happy When It’s MVVM
I think it’s fair to assume that everyone who went through Lockdown ended up rediscovering a long-lost passion or two while spending more time at home. I definitely include myself in this, as the monotony of my work-life routine had been turned on its head, the same as everyone else’s. I no longer woke up to a scantly furnished apartment, prepared myself for work, and hurried for the train while gulping down a molten Starbucks blend coffee. My office had moved from a 20 minute train ride to 20 feet away from my bed for who-knows-how-long.
After an initial adjustment, and a hurried refurnishing of my apartment, I found my creative mind swimming with motivation for the first time in months. Previously, after commuting to, working at, then commuting home from the office, I was little inclined to do yet more programming during my downtime.
These past few weeks, I found myself more motivated than ever to learn something new. I decided to further my understanding of the oft-used MVVM app architecture, and learn how to apply it to future apps I make or contribute to using Combine and SwiftUI.
Inputs & Outputs
For the last three years, I have been working on a codebase which places a heavy emphasis on I/O as a methodology for creating ViewModel objects and Service types. The approach works perfectly with ReactiveSwift, as seen below:
… but that’s not all. I knew that this approach ought to work with Combine as well, since it is a first-party solution to ReactiveSwift provided by Apple.
In essence an Input is an event which occurs on a View, such as
didTapLoginButton, and an Output is the eventual result of an Input.
@State, @ObservedObject, @EnvironmentObject?
I had some trouble wrapping my head around these annotations, but if you understand the principle of an App having a “state,” then you’ll be fine.
With SwiftUI, our views are
struct types, and thus cannot be mutated. We need to hand over control to the State, so that when it updates the view will show the latest data. Without any of this, your app would be non-functional.
If your “state” comprises of a few simple types such as a
Bool, and an
Int, then you can go ahead and annotate your properties as
@State types. As this implies that the state only refers to and controls the current screen, it’s important to mark these properties
private. Scope matters!
If your “state” is more complex, such as a ViewModel, and/or may apply to multiple views, you’ll be better off using
@ObservedObject. This is the same as
@State in many ways, but when you create properties in your state, you get to decide whether changes to each property force a refresh or not.
@EnvironmentObject is very similar to
@ObservedObject however these objects are available to all views of your app at any time. If you share a lot of data around screens of your application, these are incredibly useful. The closest comparison to
@EnvironmentObject that I can give you, is the AppState employed in apps built with a Redux architecture, or even simpler — a global singleton.
After reading about the property wrappers, I decided that my ViewModel ought to be an
@ObservableObject. When properties in the view model change, I want my components to update accordingly. Furthermore, my ViewModel likely won’t just hold simple types forever. I may eventually expand it and make it hold more complex types, or load data from a server.
The first step was to port over my
ViewModel protocol, and then find an equivalent for my
.pipe() objects from ReactiveSwift, which conveniently use the I/O principle already.
The closest match ended up being
PassthroughSubject. According to the documentation, these doesn’t have an initial value or a buffer of the most recently-published element, very similar to
.pipe() from ReactiveSwift.
More documentation goodies led me to
@Published properties, which do what they say on the tin. They “publish” values to objects which observe them. Thus, my ViewModel becomes as follows:
…and my ContentView just needs the following:
Running the app will show
Hello, World! right in the middle of the screen as you would expect.
Congratulations! You have now implemented MVVM using Combine and SwiftUI!
That’s All For Now
This is not my first foray into SwiftUI. Up until now, I have used bog-standard SwiftUI to build some debug screens (which are not user-facing) in my projects as a way to get my head around things.
As I learn more, I will publish more.
Stay tuned! In the meantime, please let me know what you think. 🤔
Update: The Next Day
Tony Arnold of Reveal fame, kindly pointed out a small adjustment I can make to my View Models to avoid having to manually add objects to the
cancellables array. It’s a neater approach, and moves my Combine+SwiftUI ViewModel even closer to what I had when using ReactiveSwift.
Thanks Tony! 🍻