One of our teams at Boltmade started a new iOS project recently. We’ve built iOS apps before, and we decided that since we were building from scratch, this was a great opportunity to try to improve upon two major pain points from prior iOS projects:

  1. Massive View Controllers: iOS’s UIViewController subclasses tend to become unwieldy beasts full of a mix of UI and business logic.

  2. Testing: UIViewContoller subclasses contain most of the logic, so they should be tested, but testing code that’s intermixed with view code is difficult.

We decided to try tackling both of these problems by using ReactiveCocoa to bring a Model-View-ViewModel (MVVM) architectural approach to this project, which promised to reduce view controller code and decouple UI and business logic for easier testing.

Tools Used

We used ReactiveCocoa 2.0 because it is currently the recommended stable version. It was built for Objective-C so it doesn’t take advantage of a lot of Swifty goodness that looks like it will come with ReactiveCocoa 3.0. In the meantime, ReactiveCocoa needed some help working with Swift, so we took a mix of extensions and structs for RAC, RACObserve and a couple other handy tidbits like RACSignal.subscribeNextAs<T> from Colin Eberhardt and Yusef Napora. We also added a couple of our own extensions.

For testing, we used Quick and Nimble. Quick gives us a Spec-style testing framework in Swift, and Nimble gives us nicer expectation syntax.

Our Approach, By Example

MVVM means many things to many people. We are applying the concept similar to how it is used in Windows Presentation Foundation (WPF) to create a distinction between the view (and view controller), which deal with UI things, and some other thing that handles business logic, network code, etc. We call that other thing the view model. To get more familiar with the concept of MVVM on iOS and why you might want to use it, we recommend the following write-ups that helped us get started: Ash Furrow’s high-level explanation of the MVC problem on iOS and how it can be solved with MVVM, and Colin Wheeler’s slightly more hands-on version using ReactiveCocoa.

To explain the way we used ReactiveCocoa and MVVM we’ll code the same application twice: once in a naive way placing all the logic within the view controller, and once in a ReactiveCocoa MVVM way. The simple app we’ll code is a dice rolling game: you have a die, and you can roll it. When you roll a 6, you win and the game is over. You have to start a new game to continue rolling the die. Here’s what it looks like.

Dice game start Dice game inprogress Dice game win

First, let’s look at a naive implemenation of this game:

NaiveViewController.swift

import UIKit

class NaiveViewController : UIViewController {

    @IBOutlet weak var currentRollLabel: UILabel!
    @IBOutlet weak var rollButton: UIButton!
    @IBOutlet weak var newGameButton: UIButton!

    var die : Die?

    override func viewDidLoad() {
        super.viewDidLoad()

        self.rollButton.addTarget(self, action: "rollButtonPressed", forControlEvents: .TouchUpInside)
        self.newGameButton.addTarget(self, action: "newGameButtonPressed", forControlEvents: .TouchUpInside)

        updateUIStates()
    }

    func updateCurrentRollLabel() {
        switch self.die?.dieValue {
        case .Some(DieValue.Six):
            self.currentRollLabel.text = "YOU WIN! (Why don't you play again?)"
            self.currentRollLabel.textColor = UIColor.greenColor()
        case (.Some(let dieValue)):
            self.currentRollLabel.text = "You rolled a \(dieValue.rawValue). Keep trying!"
            self.currentRollLabel.textColor = UIColor.blackColor()
        default:
            self.currentRollLabel.text = "You haven't rolled the die yet."
            self.currentRollLabel.textColor = UIColor.blackColor()
        }

    }

    func updateButtonState() {
        self.rollButton.enabled = self.die?.dieValue != .Six
    }

    func updateUIStates() {
        updateButtonState()
        updateCurrentRollLabel()
    }

    func rollButtonPressed() {
        if let die = self.die {
            die.roll()
        } else {
            self.die = Die()
        }

        updateUIStates()
    }

    func newGameButtonPressed() {
        self.die = nil
        updateUIStates()
    }
}

This is pretty straight forward: we have IBOutlets for a label that tells you about the game state, a button to roll, and a button to start a new game. There is a function that updates the UI state updateUIStates(), and targets on our buttons. Those targets manipulate the model object die, and then call updateUIStates() to ensure the UI is up to date.

What we don’t like about this implementation is the manual calls to updateUIStates() that happen when you modify the model. If we were to write additional game logic, we would likely have to similarly remember to update the UI, like we did in both rollButtonPressed() and newGameButtonPressed(). This also looks like it would be difficult to implement unit tests on because the tests would have to instantiate the view controller and interact with the view.

Let’s try again, but this time with ReactiveCocoa and MVVM:

MVVMViewController.swift

import UIKit

class MVVMViewController : UIViewController {
    @IBOutlet weak var currentRollLabel: UILabel!
    @IBOutlet weak var rollButton: UIButton!
    @IBOutlet weak var newGameButton: UIButton!

    var viewModel = ViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        RAC(self.currentRollLabel, "text") <~ RACObserve(self.viewModel, "currentRollDescription")
        RAC(self.currentRollLabel, "textColor") <~ self.viewModel.isGameOver.mapAs { (isGameOver : Bool!) -> UIColor! in
            return isGameOver == true ? UIColor.greenColor() : UIColor.blackColor()
        }

        self.rollButton.rac_command = self.viewModel.rollDieCommand
        self.newGameButton.rac_command = self.viewModel.newGameCommand
    }
}

Check out that tiny view controller! It still has the IBOutlets, and other than that its sole responsibilities are UI-related, which means binding the view to the view model. In a more complicated app it might also perform UIView animations, or create and configure an AVCaptureSession if you wanted to use the built-in device camera. The point is, it’s all strictly UI logic. So where does everything else go?

ViewModel.swift

import Foundation
import ReactiveCocoa

public class ViewModel : NSObject {

    public dynamic private(set) var currentRollDescription : String! = nil
    public private(set) var rollDieCommand:RACCommand!
    public private(set) var newGameCommand:RACCommand!
    public private(set) var isGameOver:RACSignal!

    public dynamic private(set) var die : Die?

    public override init() {
        super.init()

        RAC(self, "currentRollDescription") <~ RACObserveOptional(self, ["die", "dieValueRaw"]).mapAs { (dieValueRaw : Int?) -> String! in
            switch (dieValueRaw) {
            case .Some(DieValue.Six.rawValue):
                return "YOU WIN! (Why don't you play again?)"
            case .Some(let dieValueRaw):
                return "You rolled a \(dieValueRaw). Keep trying!"
            default:
                return "You haven't rolled the die yet"
            }
            }

        self.isGameOver = RACObserveOptional(self, ["die", "dieValueRaw"]).mapAs { (dieValueRaw : Int?) -> Bool! in
            switch (dieValueRaw) {
            case .Some(DieValue.Six.rawValue):
                return true
            default:
                return false
            }
            }

        self.rollDieCommand = RACCommand(enabled: isGameOver.not(), signalBlock: { (any:AnyObject!) -> RACSignal! in
            if let die = self.die {
                die.roll()
            } else {
                self.die = Die()
            }
            return RACSignal.empty()
        })

        self.newGameCommand = RACCommand(signalBlock: { (any:AnyObject!) -> RACSignal! in
            self.die = nil
            return RACSignal.empty()
        })
    }
}

In the view model we see the things that we bound the view to in the view controller: a couple of button commands, a descriptive string, and a signal that tells us when the game is over. The string currentRollDescriptionis itself bound to the private die member, so that it is always in sync with die. Then to start a new game, the newGameCommand simply throws out the current die. Notice that we don’t have to do any UI maintentance: currentRollDescription already knows that it needs to reflect the value of die at all times, so it does. In fact, notice that there is not even an import UIKit here: view models are strictly a no UI zone.

There’s a little bit more going on with rollDieCommand: if there’s a die it roll()s it, otherwise it creates and holds a Die() which rolls itself on creation, but either way the command is only enabled if isGameOver.not(). Like currentRollDescription, isGameOver is a RACSignal that reflects the current state of die: in this case, when dieValueRaw is 6, the game is over. This means that rollDieCommand is only enabled when the game hasn’t already be won. ReactiveCocoa gives you a fancy extension on UIButton called rac_command which applies the enabled state of the command to UIButton.enabled, so you don’t even have to do any additional work to enable or disable rollButton: it behaves as you would expect based on the validity of its rac_command.

There’s also a bit of ugliness in that we have to RACObserve dieValueRaw instead of dieValue. The reason for this is that we coded dieValue as an enum. ReactiveCocoa can only work with types that can be translated to Objective-C, and Swift enums are more expressive than Objective-C enums so they aren’t compatible. The work-around is to keep the enum property dieValue, but add a didSet which changes something compatible with Objective-C and observe that when you need to use ReactiveCocoa. As example, here’s the model which provides an enum property dieValue and a matching Objective-C-compatible property dieValueRaw:

Die.swift

import Foundation

public enum DieValue: Int {
    case One = 1, Two, Three, Four, Five, Six
}

public class Die : NSObject {
    dynamic public private(set) var dieValueRaw : Int
    public var dieValue : DieValue {
        didSet {
            self.dieValueRaw = self.dieValue.rawValue
        }
    }

    public override init() {
        self.dieValue = Die.getRandomRoll()
        self.dieValueRaw = self.dieValue.rawValue
        super.init()
    }

    public func roll() {
        self.dieValue = Die.getRandomRoll()
    }

    private class func getRandomRoll() -> DieValue {
        var numVal = Int(arc4random_uniform(6)+1)
        return DieValue(rawValue:numVal)!
    }
}

Overall we like this approach better than the naive approach and the traditional MVC approach for a number of reasons. It reads and writes more intuitively; it encourages us to think in terms like “what should currentRollDescription be?”, then go code it to provide a single source of truth. We’ve also completely separated the UI from our business logic. This means that the UI could be changed or rebranded without fear of modifying our business logic. It also means that we could test our business logic without dealing with views. Let’s do that!

Testing: No Excuses Now

Now that we don’t need to deal with UIKit, testing is easy! Here’s just a couple of tests to demonstrate that useful tests are possible without ever instantiating a view or view controller.

ViewModelSpec.swift

import Quick
import Nimble
import SimpleDiceGame

class ViewModelSpec: QuickSpec {
    override func spec() {
        describe("rolling the die") {

            var vm:ViewModel!

            beforeEach {
                vm = ViewModel()
            }

            it ("should create a die") {
                vm.rollDieCommand.execute(nil)
                expect(vm.die).toNot(beNil())
            }

            it ("shouldn't roll anymore once you win") {
                var error : NSError? = nil

                for var i = 0; (i < 40) && (vm.isGameOver.not().asynchronousFirstOrDefault(nil, success: nil, error: nil) as! Bool); i++ {
                    vm.rollDieCommand.execute(nil)
                }

                expect(error).to(beNil())
                vm.rollDieCommand.execute(nil).asynchronousFirstOrDefault(nil, success: nil, error: &error)
                expect(error).toNot(beNil())
            }

            it ("it should say you won when you win") {
                var error : NSError? = nil

                for var i = 0; (i < 40) && (vm.isGameOver.not().asynchronousFirstOrDefault(nil, success: nil, error: nil) as! Bool); i++ {
                    expect(vm.currentRollDescription).toNot(contain("WIN"))
                    vm.rollDieCommand.execute(nil)
                }

                expect(vm.currentRollDescription).to(contain("WIN"))
            }
        }
    }
}

Retrospective Time!

What we liked:

  • We accomplished what we set out to do: our view controllers are smaller than in past experiences and are very focused on UI logic and binding. Our new view models are decoupled, contain all the business and network logic and are better organized. We also have better test coverage.

  • We found decoupling of UI logic and business logic made collaboration better: it’s really quick and easy to review a pull request when you see a view model with a command like rollDieCommand: you don’t have to go hunting around to make sure all the edge cases are caught because the whole situation of rolling the die is described in that little piece.

  • We found the mental process of programming reactively really enjoyable: for example, in the past, we’ve naturally tended to think about things like the enabled state of a button (ex rollButton) by fully defining the situation in our heads, and then going and implementing that in all the places it’s necessary (ex on rollButtonPressed() and newGameButtonPressed() and who knows what other code in the future). Instead, we can now fully define the enabled state in isGameOver and attach it to the rollDieCommand, and now it’s done and we never have to think about it again. It’s just a really natural, confident way to think about code.

  • During the project we encountered a practical situation where we needed to represent the same data in two very different looking views. We had already built one of those views with a controller and view model. Turns out it was super quick and easy to build a new view and just attach it to the same view model. Decoupling achieved!

What we didn’t like:

  • As with most new technologies or programming strategies, the learning curve was pretty steep.

  • ReactiveCocoa 2.0 doesn’t play well with some of Swift’s nicer features, like expressive enums, generics and optionals. There are examples of each of these in ViewModel.swift above:

    • As already discussed, Swift enums aren’t supported. You can work around this with an Objective-C-compatible raw value.

    • Most of the RAC interfaces don’t leverage generics. We wrote RACStream.mapAs as a generics-friendly replacement for RACStream.map to avoid excessive casting.

extension RACStream {
    func mapAs<T, U>(block:(T!) -> U!) -> RACSignal! {
        var mappedStream = self.map { (any : AnyObject!) -> AnyObject! in
            let inAsT = any as? T
            return block(inAsT) as! AnyObject
        }
        return mappedStream as! RACSignal
    }
}
  • Observing a property on an optional using RACObserve results in run-time errors. We made RACObserveOptional to handle situations like self.die?.dieValueRaw.
func RACObserveOptional(target: NSObject!, keyPaths: [String]) -> RACSignal  {
    if (keyPaths.count == 0) {
        return RACSignal.`return`(nil)
    }

    return RACObserve(target, keyPaths[0]).flattenMap { (any) -> RACStream! in
        if let safeAny = any as? NSObject {
            var restOfKeyPaths = keyPaths
            restOfKeyPaths.removeAtIndex(0)
            if restOfKeyPaths.count >= 2 {
                return RACObserveOptional(safeAny, restOfKeyPaths)
            } else {
                return RACObserve(safeAny, restOfKeyPaths[0])
            }
        } else {
            return RACSignal.`return`(nil)
        }
    }
}
  • ReactiveCocoa and UITableViews didn’t play as nicely as we would have liked. Table views have all those delegate and datasource methods you have to implement, which quickly cause your tidy view controller to expand in size. It’s not awful: people have come up with strategies to keep your view controller light, but we found the strategy broke down in table views with highly interactive cells. It wasn’t the end of the world, it was just clunky.

  • We had some brutal problems getting off the ground with Quick, Nimble and ReactiveCocoa due to some confusion with static vs dynamic library linking. More on that here.

Conclusion

We loved taking an MVVM approach to iOS development. After an initial learning hump, ReactiveCocoa became easy to use, and the MVVM mental model allowed us to write code that read more naturally and was easier to maintain. We also liked having unit tests because, as intended, they sometimes caught bugs before we did. Overall it was a great experience and we’ll definitely take this approach again.