We’ve all seen View Controllers consisting of hundred or even thousands of lines of code. One popular strategy of reducing view controller complexity is using the Model-View-ViewModel (MVVM) design pattern. But that’s not the only way to separate the view controllers into smaller, easier to understand components. This post will explore using small Presentation Control classes to achieve a similar effect. They can even be used together with MVVM components.
This post will not cover the MVVM pattern in great detail. If you’d like to read more about that please follow one of the links at the end of this post. However, you should be able to understand most of this post even without prior knowledge of MVVM. First we’ll show a traditional example of a view controller, followed by an example using MVVM and then another example using Presentation Controls. At the end we’ll compare them and see how we can use them together.
Problem: Complex view controller
The problem we’re trying to solve here is that view controllers often contain too much functionality. Besides updating the view and handling user interaction, they’re often full of state, network requests, error handling, validation, you name it.
Let’s have a look at an example of a traditional view controller. Imagine we have a travel planner app where we show the departure and arrival time of a trip that we’re about to make. This could look something like this:
All this information is taken from a pretty simple model:
[code language="obj-c"]
class Trip {
let departure: NSDate
let arrival: NSDate
let actualDeparture: NSDate
init(departure: NSDate, arrival: NSDate, actualDeparture: NSDate? = nil) {
self.departure = departure
self.arrival = arrival
self.actualDeparture = actualDeparture ?? departure
}
}
[/code]
The model has a departure time, arrival time and an actual departure time. The view shows the date at the right top (we assume the date of departure and arrival is always the same for the sake of keeping things simple), the departure time on the left and arrival time on the right. In between the departure and arrival times you see the duration. If the actual departure time is later than the departure time it means there is a delay. In that case the departure time is displayed in red and the delay displayed at the bottom.
Without MVVM or Presentation Controls the code in our view controller would look as follows:
[code language="obj-c"]
class ViewController: UIViewController {
@IBOutlet weak var dateLabel: UILabel!
@IBOutlet weak var departureTimeLabel: UILabel!
@IBOutlet weak var arrivalTimeLabel: UILabel!
@IBOutlet weak var durationLabel: UILabel!
@IBOutlet weak var delayLabel: UILabel!
// in a real app the trip would be set from another view controller or loaded from a server or something…
let trip = Trip(departure: NSDate(timeIntervalSince1970: 1444396193), arrival: NSDate(timeIntervalSince1970: 1444397193), actualDeparture: NSDate(timeIntervalSince1970: 1444396493))
override func viewDidLoad() {
super.viewDidLoad()
dateLabel.text = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .ShortStyle, timeStyle: .NoStyle)
departureTimeLabel.text = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .NoStyle, timeStyle: .ShortStyle)
arrivalTimeLabel.text = NSDateFormatter.localizedStringFromDate(trip.arrival, dateStyle: .NoStyle, timeStyle: .ShortStyle)
let durationFormatter = NSDateComponentsFormatter()
durationFormatter.allowedUnits = [.Hour, .Minute]
durationFormatter.unitsStyle = .Short
durationLabel.text = durationFormatter.stringFromDate(trip.departure, toDate: trip.arrival)
let delay = trip.actualDeparture.timeIntervalSinceDate(trip.departure)
if delay > 0 {
durationFormatter.unitsStyle = .Full
delayLabel.text = String.localizedStringWithFormat(NSLocalizedString("%@ delay", comment: "Show the delay"), durationFormatter.stringFromTimeInterval(delay)!)
departureTimeLabel.textColor = .redColor()
} else {
delayLabel.hidden = true
}
}
}
[/code]
Luckily we’re using a Storyboard with constraints and don’t need any code to position any of the views. And if this would be all, we probably wouldn’t have to change anything as this is still small enough to maintain. But in real apps, view controllers do a lot more than this, in which case it’s good to simplify things and separate different concerns. That will also improve testability a lot.
Solution 1: Use MVVM
We can improve our code by using the MVVM pattern. This would look something like this:
[code language="obj-c"]
class TripViewViewModel {
let date: String
let departure: String
let arrival: String
let duration: String
let delay: String?
let delayHidden: Bool
let departureTimeColor: UIColor
init(_ trip: Trip) {
date = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .ShortStyle, timeStyle: .NoStyle)
departure = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .NoStyle, timeStyle: .ShortStyle)
arrival = NSDateFormatter.localizedStringFromDate(trip.arrival, dateStyle: .NoStyle, timeStyle: .ShortStyle)
let durationFormatter = NSDateComponentsFormatter()
durationFormatter.allowedUnits = [.Hour, .Minute]
durationFormatter.unitsStyle = .Short
duration = durationFormatter.stringFromDate(trip.departure, toDate: trip.arrival)!
let delay = trip.actualDeparture.timeIntervalSinceDate(trip.departure)
if delay > 0 {
durationFormatter.unitsStyle = .Full
self.delay = String.localizedStringWithFormat(NSLocalizedString("%@ delay", comment: "Show the delay"), durationFormatter.stringFromTimeInterval(delay)!)
departureTimeColor = .redColor()
delayHidden = false
} else {
self.delay = nil
departureTimeColor = UIColor(red: 0, green: 0, blue: 0.4, alpha: 1)
delayHidden = true
}
}
}
[/code]
We’ve created a separate class TripViewViewModel that translates everything from our Trip model to things we can use in our view. Note that this new class doesn’t know anything about UIView classes like UILabels etc. This binding is still handled in our View Controller, which now looks like this:
[code language="obj-c"]
class ViewController: UIViewController {
@IBOutlet weak var dateLabel: UILabel!
@IBOutlet weak var departureTimeLabel: UILabel!
@IBOutlet weak var arrivalTimeLabel: UILabel!
@IBOutlet weak var durationLabel: UILabel!
@IBOutlet weak var delayLabel: UILabel!
let tripModel = TripViewViewModel(Trip(departure: NSDate(timeIntervalSince1970: 1444396193), arrival: NSDate(timeIntervalSince1970: 1444397193), actualDeparture: NSDate(timeIntervalSince1970: 1444396493)))
override func viewDidLoad() {
super.viewDidLoad()
dateLabel.text = tripModel.date
departureTimeLabel.text = tripModel.departure
arrivalTimeLabel.text = tripModel.arrival
durationLabel.text = tripModel.duration
delayLabel.text = tripModel.delay
delayLabel.hidden = tripModel.delayHidden
departureTimeLabel.textColor = tripModel.departureTimeColor
}
}
[/code]
Since this is now a one-to-one binding from MVVM properties to UIView (in this case UILabel) properties, our view controller’s complexity has been greatly reduced. And our TripViewViewModel is really easy to unit test.
Move logic to the model
Our current TripViewViewModel class contains some logic that might fit better in our Trip model. Especially all the calculations that are not directly related to the presentation.
[code language="obj-c"]
class Trip {
let departure: NSDate
let arrival: NSDate
let actualDeparture: NSDate
let delay: NSTimeInterval
let delayed: Bool
let duration: NSTimeInterval
init(departure: NSDate, arrival: NSDate, actualDeparture: NSDate? = nil) {
self.departure = departure
self.arrival = arrival
self.actualDeparture = actualDeparture ?? departure
// calculations
duration = self.arrival.timeIntervalSinceDate(self.departure)
delay = self.actualDeparture.timeIntervalSinceDate(self.departure)
delayed = delay > 0
}
}
[/code]
Here we moved the logic of calculating the duration and delay to the Trip model.
The presentation logic remains in our TripViewViewModel, which is now using the calculated properties of the model:
[code language="obj-c"]
class TripViewViewModel {
let date: String
let departure: String
let arrival: String
let duration: String
let delay: String?
let delayHidden: Bool
let departureTimeColor: UIColor
init(_ trip: Trip) {
date = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .ShortStyle, timeStyle: .NoStyle)
departure = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .NoStyle, timeStyle: .ShortStyle)
arrival = NSDateFormatter.localizedStringFromDate(trip.arrival, dateStyle: .NoStyle, timeStyle: .ShortStyle)
let durationFormatter = NSDateComponentsFormatter()
durationFormatter.allowedUnits = [.Hour, .Minute]
durationFormatter.unitsStyle = .Short
duration = durationFormatter.stringFromTimeInterval(trip.duration)!
delayHidden = !trip.delayed
if trip.delayed {
durationFormatter.unitsStyle = .Full
delay = String.localizedStringWithFormat(NSLocalizedString("%@ delay", comment: "Show the delay"), durationFormatter.stringFromTimeInterval(trip.delay)!)
departureTimeColor = .redColor()
} else {
self.delay = nil
departureTimeColor = UIColor(red: 0, green: 0, blue: 0.4, alpha: 1)
}
}
}
[/code]
Since the properties of the TripViewViewModel didn’t change, our View Controller remains exactly the same.
Solution 2: Presentation Controls
If you’re like me, you might feel that setting certain properties like our delayHidden and departureTimeColor in TripViewViewModel is a bit odd. These clearly just translate to the hidden and textColor properties of UILabels. And we also need a lot of extra code just for copying property values.
So what can we do to bring the presentation logic closer to our UIView classes?
Small classes that control the binding between the model and the views is the answer: Presentation Controls. The main difference between MVVM classes and Presentation Controls is that the latter does know about UIView classes. Presentation Controls are like mini View Controllers that are only responsible for the presentation.
You could achieve a similar effect by creating custom UIView subclasses that are a combination of views. This worked quite well with views that were completely written in code or views created from a nib file, but it doesn’t work well at all in combination with Storyboards. While we want to remove some of the complexity from our view controller, we don’t want to move our individual views in a storyboard scene to a separate nib.
Something you might not know is that you can create outlets from views in your scene to objects other than the View Controller of that scene. You can connect them to any object that you add to your scene. Lets first create our presentation control, which is a new class that we call TripPresentationControl:
[code language="obj-c"]
class TripPresentationControl: NSObject {
}
[/code]
Make sure it’s a subclass of NSObject, otherwise you’ll have problems creating it from Interface Builder.
Now go to the Object Library in Interface Builder and drag a new Object to your scene.
Change its class in the Identity Inspector to the presentation control class: TripPresentationControl.
You can now use it pretty much the same as you would a view controller. Just drag outlets from your scene to this class. By doing so, we’ll move all of our labels to the presentation control and connect them to the scene.
[code language="obj-c"]
class TripPresentationControl: NSObject {
@IBOutlet weak var dateLabel: UILabel!
@IBOutlet weak var departureTimeLabel: UILabel!
@IBOutlet weak var arrivalTimeLabel: UILabel!
@IBOutlet weak var durationLabel: UILabel!
@IBOutlet weak var delayLabel: UILabel!
var trip: Trip! {
didSet {
dateLabel.text = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .ShortStyle, timeStyle: .NoStyle)
departureTimeLabel.text = NSDateFormatter.localizedStringFromDate(trip.departure, dateStyle: .NoStyle, timeStyle: .ShortStyle)
arrivalTimeLabel.text = NSDateFormatter.localizedStringFromDate(trip.arrival, dateStyle: .NoStyle, timeStyle: .ShortStyle)
let durationFormatter = NSDateComponentsFormatter()
durationFormatter.allowedUnits = [.Hour, .Minute]
durationFormatter.unitsStyle = .Short
durationLabel.text = durationFormatter.stringFromTimeInterval(trip.duration)!
delayLabel.hidden = !trip.delayed
if trip.delayed {
durationFormatter.unitsStyle = .Full
delayLabel.text = String.localizedStringWithFormat(NSLocalizedString("%@ delay", comment: "Show the delay"), durationFormatter.stringFromTimeInterval(trip.delay)!)
departureTimeLabel.textColor = .redColor()
}
}
}
}
[/code]
Of course you can create multiple presentation controls to split complex view controllers into manageable smaller controls.
As you can see, we also created a trip property that contains all our presentation logic, pretty much the same we had in our TripViewViewModel. But now we directly set values on the labels.
The next step is to create an outlet from our Presentation Control from the scene to the view controller.
Upon loading our view we will pass the trip to the presentation control. Our entire view controller now looks like this:
[code language="obj-c"]
class ViewController: UIViewController {
@IBOutlet var tripPresentationControl: TripPresentationControl!
let trip = Trip(departure: NSDate(timeIntervalSince1970: 1444396193), arrival: NSDate(timeIntervalSince1970: 1444397193), actualDeparture: NSDate(timeIntervalSince1970: 1444396493))
override func viewDidLoad() {
super.viewDidLoad()
tripPresentationControl.trip = trip
}
}
[/code]
Pretty clean right?
So what about MVVM?
So should you stop using MVVM? Not necessarily. MVVM and Presentation control both have their own strengths and you can even use them together. With MVVM alone, you probably will write slightly more code than with Presentation Controls alone. This does make MVVM a bit easier to test since you don’t need to instantiate any UIViews. If you want to test Presentation Controls, you will have to create the views, but the great thing is that these can be simple dummy views. You don’t need to replicate the entire view hierarchy or instantiate your view controllers. And most properties you set on the UIViews you can easily test, like the text of a UILabel.
In many large projects the MVVM classes and Presentation Controls can work perfectly together. In that case you would pass on the MVVM object to your Presentation Control. The Presentation Control would then be the layer in between your views and the MVVM and its Model. Things like network requests are also much better handled in an MVVM object since you probably don’t want to have the complexity of networks requests and views together in one component.
Conclusion
It’s completely up to you and the size of your project whether you use MVVM, Presentation Controls or both. If you currently have view controllers with over a 1000 lines of code, then using both might be a good idea. But if you only have very simple view controllers than you might just want to stick with the traditional View Controller patterns.
In the near future I will write a follow-up post that will go into more detail about how to deal with model changes and user interaction in Presentation Controls.
To read more about MVVM in iOS, I recommend you to read Introduction to MVVM by Ash Furrow and From MVC to MVVM in Swift by Srdan Rasic.
Also make sure to join the do {iOS} Conference in Amsterdam the 9th of November, 2015. Here Natasha "the Robot" Murashev will be giving a talk about Protocol-oriented MVVM. If you enjoyed reading this post you’ll definitely want to see her session about MVVM.