Model View ViewModel has become the default way I write apps on iOS – it makes writing iOS apps a joy. I've written about it again and again and oh my.
But last Autumn, as our team was all-hands-on-deck to wrap up the auctions kiosk app, we chose not to use MVVM. Why not?
We were building a new app in a new language using a non-Swift framework for functional reactive programming. I was also teaching colleagues what they needed to know about ReactiveCocoa to help write the app. We used the MVC pattern because the relative cost of MVVM was too high.
"... was ..."
Since then, I've been pecking away at converting small view controllers away from MVC, to figure out what MVVM on iOS written in Swift might look like. My goal has been getting ready to cut down our behemoth main view controller and create a view model for it. Before the rewrite, it was nearly 600 lines of code and was responsible for:
- networking.
- syncing auction lot states.
- user interaction.
- collection view layouts.
- image caching.
- background-thread data processing.
It's quite terrifying now that I think about it!
Well, finally I was ready. The view controller is now down to 224 lines, and is responsible for only things like displaying data. Calculating what to display and when to display it is now contained within the view model. In true MVVM form, our view controller doesn't even have direct access to the models it displays!
So what does MVVM in Swift look like? Well, our answer is just that – our answer. Others exist, and they have merits and faults of their own.
I'm not here to preach a definitive definition of MVVM in Swift. Instead, I want to talk about some of the lessons we learnt in the process of building a solution that worked for us.
Programming boils down to getting certain bits of code to communicate with other bits in an intelligent way. Software patterns are ways of constraining programmers to make it easier to program.
MVVM, roughly, has the following constraints:
- Models don't talk to anybody (same as MVC).
- View models only talk to models.
- View controllers can't talk to models directly; they only interact with view models and views.
- Views only talk to the view controllers, notifying them of interaction events (same as MVC).
And that's pretty much it. It's not that different from MVC – the key differences are:
- There's a new "view model" class.
- The view controller no longer has access to the model.
Additionally, MVVM on iOS acknowledges the one-to-one relationship between views and view controllers. I tend to think of them as one entity that just happens to be split across a .swift
file and a Storyboard.
The view model's job is to handle all presentation logic. If a model contains an NSDate
, the NSDateFormatter
to format that date would live in the view model.
View models don't have any access to the user interface. You should not even import UIKit
in a view model. Typically, a view controller observes the view model somehow to know when there's new data to display. This can be done through KVO or FRP.
MVVM and MVC share a common weakness: neither defines where the network logic of an app should go. I've put it in the view model for now, but I plan on separating it out into its own object soon. That object will be owned by the view model.
So let's talk about some specific challenges we had.
User Interface Structure
Part of our user interface consists of a segment control near the top of the screen. The currently selected segment determines the sort order of the collection view cells, as well as the collection view's layout. We had previously defined an enum to store the titles and sort order corresponding to each segmented control; the order of the enum cases implies the order of the controls in the UI.
1 2 3 4 5 6 7 8 |
|
So where does this enum live in MVVM? Since the logic for sorting models, the button titles, and the order of the buttons are all pieces of presentation logic, the enum seems like it belongs in the view model.
However, the decision of which layout for the collection view to use is slightly more nuanced. The layout doesn't affect what data we show the user or how they interact with it; it only affects the visuals how the information is presented. This suggests the logic for deciding layouts might belong in the view controller.
My solution was to put the enum in the view model, and have the view model expose a signal defining which of the two layouts should be used. Based on the selected segment index, the view model decides which layout should be used and sends that value on a signal. The view controller is responsible for mapping that signal into a configured layout, then setting that layout on the collection view.
1 2 3 4 5 6 7 8 9 10 11 |
|
The view controller also uses this signal to define which cell reuse identifier should be used.
1 2 3 4 5 6 7 8 9 |
|
Structuring a View Model
The most common question from iOS developers about MVVM and FRP concerns how the view model exposes data to the view controller. The view controller needs to be notified about changes to the underlying model, but what mechanism do we use to do this? There are two options:
- Use (dynamic) properties on the view model, which can be observed using KVO (or wrapped in a signal/sequence using FRP).
- Use signals/sequences/futures as properties on the view model, which can be consumed by their corresponding async frameworks.
The first option is appealing since it gives your view controller a choice of how to observe the properties. However, I'd recommend against it; Swift doesn't have type-checking on KVO (you need to cast from AnyObject!
a lot).
The second option is what I prefer, and it seems the most "Swift" way of doing things. When we do move away from RAC's Objective-C interface, the view model will replace its RACSignal
properties with sequences based on Swift generics, which will provide compile-time type-checking 💯
Defining these signals on a view model can be tricky. Swift initializers have strict rules around when properties are assigned. The signals need access to the internal state of the view model, so they need to be created after calling super.init()
. However, we can't call super.init()
until all our properties have been assigned to, including the signal properties.
It's your standard chicken-and-the-egg problem 🐣
I took the easy way out and used implicitly-unwrapped optionals, defined with var
, which can be assigned to after the call to super.init()
. It's not a perfect solution. We could instead use lazy var
properties assigned to self-evaluating closures, or just use computed properties. I'm hoping to explore other options when we move away from RAC 2's Objective-C API.
Handling User Interaction
The next problem I had was presenting details based on user interaction. Users tap a button, which is handled in the view controller, which presents the details. However, the view controller should not have access to the models, so how can it configure the details to present them?
My solution took advantage of the interchangeability of Swift functions and closures. First I defined a closure type in the view model.
1
|
|
Then I added a property to the view model and a corresponding parameter to the initializer.
1 2 3 4 5 6 |
|
Next I need to actually call the closure. I defined a function on the view model that the view controller can invoke, passing in the context that's necessary to decide which model's details should be presented. This context is just an index path.
1 2 3 |
|
Nice! So now when the user selects a cell, we can call this function on the view model with the index path that the user selected. The view model decides which model to use, and calls the closure.
The final piece of the puzzle is being clever about creating the view model. We need to pass a closure to its initializer, one that shows the model's details. I defined a function on the view controller that matched the ShowDetailsClosure
signature.
1 2 3 |
|
And then use lazy loading (discussed below) to call the view model's initializer. I pass in a reference to the above function as the closure parameter.
1 2 3 |
|
So let's review what happens when a user makes a selection.
Comments