Enum-Driven View Controllers

Have you ever seen the term ‘MVVM’ (Model-View-ViewModel) and been intimidated by yet another acronym in our industry that you don’t understand? In this article, I’ll explain how you are very likely already doing MVVM, and you’ll see how to tidy it up into a neat little state machine.

What you’re probably doing

You’re probably doing something like this in your view controllers. Nothing wrong with it, it’s a good place to start:

class ProductListViewController: UIViewController {
    var isLoaded: Bool = false
    var isLoading: Bool = false
    var products: [Product]?
    var error: NSError?
    ...
}

See those four instance variables? Those are your ViewModel - see, you’re doing MVVM already, without realising it - no big deal.

So here’s a few stabs at a working definition of a ViewModel: It’s the variables that drive what is being viewed. Or the model for what’s happening in your views. As opposed to your real model, which has eg Products, Customers, Orders, etc.

Your VC is probably a state machine

A ‘state machine’ is one of those computer-science concepts that goes pretty deep. But for our purposes, all I mean is that your view controller has a limited set of possible states it can be in.

Here’s a good analogy: It’s very much like the gear selector in your car, you only have limited options: F, N, R, D (or 1..5+R if you love driving a manual!).

So what are the kind of states you’re likely to see in a view controller:

  • Loading
  • Empty (loaded but there’s no data, eg inbox zero!)
  • Loaded
  • Error
  • Not logged in (eg cannot load)

So lets bring the ViewModel and state machine concepts together into one nice package.

Enums to the rescue

Now if the above sounds like an Enum, you’re right! So lets tidy up our original bunch of variables into an enum and a single state variable - emphasis on single:

class ProductListViewController: UIViewController {
    enum State {
        case Loading
        case Empty
        case Loaded([Product])
        case Error(NSError)
    }
    var state = State.Loading
    ...
}

One advantage here is that there is zero ambiguity about which state you’re in. In the earlier example, it is possible for isLoaded and isLoading to both be true if you make a coding mistake, which is a confusing situation. But with an enum that is simply impossible.

Make the Enum drive the views

Next, I recommend using a didSet handler on the variable to update your UI. Eg:

var state = State.Loading {
    didSet {
        ... update views ...
    }
}

Now it’s a simple matter of simply setting the value of the state variable whenever you want your UI to change. Eg your data fetching code will look as simple as the following:

func loadProducts() {
    state = .Loading
    ProductManager.sharedManager.requestProducts(success: { products in
        if products.count > 0 {
            self.state = .Loaded(products)
        } else {
            self.state = .Empty
        }
    }, failure: { error in
        self.state = .Error(error)
    })
}

To make the above example make more sense, here’s some example code for the product manager:

struct Product {
    // ...
}

class ProductManager {
    static let sharedManager = ProductManager()
    func requestProducts(
		    success success: [Product] -> (),
		    failure: NSError -> ()) {
        // ...
    }
}

Table example

And for the sake of a half-fleshed-out example, here’s something I commonly do: I have a view controller with a table view. For the loaded state, normal data rows show. For error state, one entire-screen-height cell shows with an error message. For empty state, one big cell with a helpful message shows. And for loading state, we have one big cell with an activity indicator. It all comes together beautifully as we’ll go over now:

When setting the state, all that is required to update is to call the table’s reloadData method:

var state = State.Loading {
    didSet {
        tableView?.reloadData()
    }
}

The table data source then looks like below. It is responsible for showing one special cell for the loading/empty/error states, as well as the typical product cells:

extension ProductsViewController: UITableViewDataSource {

    func tableView(tableView: UITableView,
		    numberOfRowsInSection section: Int) -> Int {
        switch state {
        case .Loading, .Empty, .Error:
            return 1
        case .Loaded(let items):
            return items.count
        }
    }
    
    func tableView(tableView: UITableView,
		    cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        switch state {
        case .Loading:
            return tableView.dequeueReusableCellWithIdentifier(LoadingCell.cellId, forIndexPath: indexPath)
        case .Error(let error):
            let cell = tableView.dequeueReusableCellWithIdentifier(CaptionCell.cellId, forIndexPath: indexPath) as! CaptionCell
            cell.caption.text = error.localizedDescription
            return cell
        case .Empty:
            let cell = tableView.dequeueReusableCellWithIdentifier(CaptionCell.cellId, forIndexPath: indexPath) as! CaptionCell
            cell.caption.text = "There are no products to view today, sorry!"
            return cell
        case .Loaded(let products):
            let product = products[indexPath.row]
            let cell = tableView.dequeueReusableCellWithIdentifier(ProductCell.cellId, forIndexPath: indexPath) as! ProductCell
            cell.textLabel?.text = product.name
            cell.detailTextLabel?.text = product.description
            return cell
        }
    }
        
}

And the table view delegate is responsible for making those special cells fill the whole screen:

extension ProductViewController: UITableViewDelegate {
    
    func tableView(tableView: UITableView,
		    heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        switch state {
        case .Loading, .Empty, .Error:
            return tableView.bounds.height
        case .Loaded:
            return tableView.rowHeight
        }
    }
    
}

And that’s pretty much it for this post! Read on if you’re curious about advanced enums.

Nested enums

A friend asked me to write about this one: An interesting technique you can use is nested enums. Now it can be a bit over-the-top, so use it judiciously, but here goes:

Say your state machine, when drawn out on paper, consists of maybe two ‘top-level’ states, but if you drill down there are more subtle states that are possible. Basically a hierarchy of states, like so:

Logged in
	Playing
	Paused
	Stopped
Logged out
	Registered
	Unregistered

You may want to consider nesting your enums like so:

enum UserState {
    case LoggedIn(LoggedInState)
    case LoggedOut(LoggedOutState)
}

enum LoggedInState {
    case Playing
    case Paused
    case Stopped
}

enum LoggedOutState {
    case Unregistered
    case Registered
}

var x = UserState.LoggedIn(.Playing)
var y = UserState.LoggedIn(.Stopped)
var z = UserState.LoggedOut(.Unregistered)

I’ll leave this as an exercise to the reader.

Hope this has been helpful!

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.

Chris Hulbert

(Comp Sci, Hons - UTS)

Software Developer (Freelancer / Contractor) in Australia.

I have worked at places such as Google, Cochlear, Assembly Payments, News Corp, Fox Sports, NineMSN, FetchTV, Coles, Woolworths, Trust Bank, and Westpac, among others. If you're looking for help developing an iOS app, drop me a line!

Get in touch:
[email protected]
github.com/chrishulbert
linkedin



 Subscribe via RSS