Skip to content

Instantly share code, notes, and snippets.

@ashfurrow
Last active September 24, 2015 16:39
Show Gist options
  • Save ashfurrow/655fc2daac4433d8b03a to your computer and use it in GitHub Desktop.
Save ashfurrow/655fc2daac4433d8b03a to your computer and use it in GitHub Desktop.

Model View ViewModel has become the default way I write apps on iOS. It's a paradigm that makes writing iOS apps a joy. I've written about it and again and again and oh my. Clearly, it's something that I know and love.

But last Autumn, as our team was all-hands-on-deck to wrap up the auctions kiosk app, it's not a paradigm we used. Why not?

I was building a new app in a new language using a non-Swift FRP framework, while also teaching colleagues what they needed to know about ReactiveCocoa to help me. We stuck with the MVC pattern because the opportunity cost was too high.

"... was ..."

Since then, I've been pecking away at converting small view controllers to figure out what MVVM on iOS written in Swift might look like. The goal was getting ready to cut down our behemoth main view controller and create a view model for it. Before the refactor, it was nearly 600 lines of code and was responsible for networking, syncing auction lot states, user interaction, collection view layouts, image caching, and background-thread data processing. It's quite terrifying now that I think of it.

Well, finally I was ready. The view controller is down to 224 lines, and is responsible for only things like displaying data. Calculating what to display and when to do it has all be shifted to 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 our we learnt in the process of building something 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 us programmers in ways that make it easier to think about our code.

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 interact with view models instead.
  • 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 – there's a new "view model" class, and the view controller no longer touches the model.

MVVM Diagram

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 and a Storyboard.

The view model's job is to handle all presentation logic. If a model contains an NSDate, the view model is where the NSDateFormatter would live.

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 one weakness: neither defines where the network logic goes. I put it in the view model for now, but I plan on separating it out into its own object soon.


So let's talk about some specific challenges we had.

User Interface Structure

Part of our user interface consists of a series of buttons near the top of the screen. The currently selected button 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 button; the order of the enum cases inferred the order of the buttons in the UI.

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 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 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 button, the view model decides which layout should be used. The view controller is responsible for mapping that signal into a configured layout, and setting that layout to the collection view.

// Respond to changes in layout, driven by switch selection.
viewModel.gridSelectedSignal.map { [weak self] (gridSelected) -> AnyObject! in
    switch gridSelected as? Bool {
    case .Some(true):
        return ListingsViewController.masonryLayout()
    default:
        return ListingsViewController.tableLayout(CGRectGetWidth(self?.switchView.frame ?? CGRectZero))
    }
}.subscribeNext { [weak self] layout -> Void in
    self?.collectionView.setCollectionViewLayout(layout as! UICollectionViewLayout, animated: false)
}

The view controller also uses this signal to define which cell reuse identifier should be used.

// Map switch selection to cell reuse identifier.
RAC(self, "cellIdentifier") <~ viewModel.gridSelectedSignal.map { gridSelected -> AnyObject! in
    switch gridSelected as? Bool {
    case .Some(true):
        return MasonryCellIdentifier
    default:
        return TableCellIdentifier
    }
}

Structuring a View Model

The most common question people ask me about MVVM and FRP on iOS concerns how the view model exposes data to the view controller. You essentially have two options:

  1. Use (dynamic) properties on the view model, which can be observed using KVO (probably wrapped in a sequence/signal).
  2. Use signals/sequences/futures properties on the view model, which can be subscribed to by whatever async framework you like.

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, since 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 move away from RAC's Objective-C interface, the view model will replace its RACSignal properties with generic sequences that provide compile-time type-checking.

Defining these signals is tricky. Swift initializers have strict rules around how properties are assigned. The signals are based on the internal state of the view model, so they need to be assigned after calling super.init(). However, we can't call super.init() until all our properties have been assigned to.

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 an elegant solution, and I'm keeping an eye out for a better solution. I'm hoping moving away from RAC 2's Objective-C API will help.

Handling User Interaction

The next problem I had was presenting details based on user selection. The button tap is handled in the view controller, as is the presentation of the detail view controller. However, the view controller should not have access to the models, so how can it configure the view controller its about to present?

My solution took advantage of the interchangeability of Swift functions and closures. First I defined a closure type in the view model.

typealias ShowDetailsClosure = (SaleArtwork) -> Void

Then I added a property to the view model, set to a parameter of the initializer's.

class ListingsViewModel {
    let showDetails: ShowDetailsClosure
    
    init(...
         showDetails: ShowDetailsClosure,
         ...

Next I need to call the closure when appropriate. I defined a function on the view model that the view controller can call, passing in the context necessary to decide which model should be used to configure the details view controller.

func showDetailsForSaleArtworkAtIndexPath(indexPath: NSIndexPath) {
    showDetails(sortedSaleArtworks[indexPath.item])
}

Nice! So now when the user selects a cell, we can call this function on our view model with the index path 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 calling the view model's initializer. We need to give it a closure that shows a detail view controller. I defined a view controller function that matched the ShowDetailsClosure type.

func showDetailsForSaleArtwork(saleArtwork: SaleArtwork) {
    performSegueWithIdentifier(SegueIdentifier.ShowSaleArtworkDetails.rawValue, sender: saleArtwork)
}

And then use lazy loading (discussed below) to call the view model's initializer.

lazy var viewModel: ListingsViewModelType = {
    return ListingsViewModel(..., showDetails: self.showDetailsForSaleArtwork, ...)
}()

So let's review what happens when a user makes a selection.

Selection diagram

Here are the steps:

  1. User taps a cell.
  2. A callback on the view controller is invoked with the selected index path.
  3. The view controller tells the view model which index path was selected.
  4. The view model looks up the corresponding model.
  5. The view model invokes the showDetails closure given to it in initialization.
  6. The showDetails closure performs a segue with the model.

It's not an ideal solution, I suppose, since it still exposes the model to the view controller (even under very strict conditions), but it's a reasonable compromise. As we continue to use more view models, I'm curious to see how this solution scales.

Testing

Earlier I mentioned the lazy closure property in the view controller. This is a trick that lets the view controller customize the view model it creates by passing in references to self.

lazy var viewModel: ListingsViewModelType = {
    return ListingsViewModel(
        selectedIndexSignal: self.switchView.selectedIndexSignal, 
        showDetails: self.showDetailsForSaleArtwork, 
        presentModal: self.presentModalForSaleArtwork
    )
}()

The viewModel property is first accessed by the view controller in viewDidLoad(), which means that we can replace the property by a test double any time before that.

The view controller is tested using snapshots to verify the user interface hasn't been inadvertently changed. Testing is straightforward:

  1. Create a view controller to test.
  2. Create a stubbed view model, customized for each test.
  3. Give the view controller the stubbed view model.
  4. Verify the view controller renders correctly.

While writing tests, I found it difficult to subclass the existing view model (for stubbing purposes). Since the view model initializer has side-effects (starting recurring network requests), I can't call super.init(). Instead, I made a ListingsViewModelType protocol. The view controller only interacts with the view model through this protocol. Now creating a stubbed view model is as easy as conforming to a protocol.

Seems more "Swift", too.

Now that the view model and view controller are separate, we no longer have presentation logic tests on the view controller at all. The view model's responsibilities of network requests, data processing, etc are now all tested independently of a user interface.

In my opinion, the key benefits of MVVM boil down to the following:

  1. Separating the view model from the interface makes it's easier to test presentation logic.
  2. Separating the view controller from the presentation logic makes it easier to test the interface.

This is only a description of what we've come up with so far. As Swift and the community continue to evolve, we'll re-evaluate our solutions. For now, I'm quite pleased with the step we've taken, and I'm looking forward to continuing to explore programming patterns in Swift.

@orta
Copy link

orta commented Sep 16, 2015

I love these pictures

@ashfurrow
Copy link
Author

The tho options for how to structure VMs needs clarification.

It's not an elegant solution

Another solution could be to use lazy or computed properties that are based off of state stored before super.init() is call.

The view model's responsibilities

Awkward wording

As Swift and the community

-> As our community and Swift itself continue to evolve, hand-in-hand, we'll reevaluate our solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment