In this post I’ll explain how to deal with updates when you’re using Presentation Controls in iOS. It’s a continuation of my previous post in which I described how you can use Presentation Controls instead of MVVM or in combination with MVVM.
The previous post didn’t deal with any updates. But most often the things displayed on screen can change. This can happen because new data is fetched from a server, through user interaction or maybe automatically over time. To make that work, we need to inform our Presentation Controls of any updates of our model objects.
Let’s use the Trip from the previous post again:
[code language="obj-c"]
struct Trip {
let departure: NSDate
let arrival: NSDate
let duration: NSTimeInterval
var actualDeparture: NSDate
var delay: NSTimeInterval {
return self.actualDeparture.timeIntervalSinceDate(self.departure)
}
var delayed: Bool {
return delay > 0
}
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)
}
}
[/code]
Instead of calculating and setting the delay and delayed properties in the init we changed them into computed properties. That’s because we’ll change the value of the actualDeparture property in the next examples and want to display the new value of the delay property as well.
So how do we get notified of changes within Trip? A nice approach to do that is through binding. You could use ReactiveCocoa to do that but to keep things simple in this post I’ll use a class Dynamic that was introduced in a post about Bindings, Generics, Swift and MVVM by Srdan Rasic (many things in my post are inspired by the things he writes so make sure to read his great post). The Dynamic looks as follows:
[code language="obj-c"]
class Dynamic<T> {
typealias Listener = T -> Void
var listener: Listener?
func bind(listener: Listener?) {
self.listener = listener
}
func bindAndFire(listener: Listener?) {
self.listener = listener
listener?(value)
}
var value: T {
didSet {
listener?(value)
}
}
init( v: T) {
value = v
}
}
[/code]
This allows us to register a listener which is informed of any change of the value. A quick example of its usage:
[code language="obj-c"]
let delay = Dynamic("+5 minutes")
delay.bindAndFire {
print("Delay: ($0)")
}
delay.value = "+6 minutes" // will print ‘Delay: +6 minutes’
[/code]
Our Presentation Control was using a TripViewViewModel class to get all the values that it had to display in our view. These properties were all simple constants with types such as String and Bool that would never change. We can replace the properties that can change with a Dynamic property.
In reality we would probably make all properties dynamic and fetch a new Trip from our server and use that to set all the values of all Dynamic properties, but in our example we’ll only change the actualDeparture of the Trip and create dynamic properties for the delay and delayed properties. This will allow you to see exactly what is happening later on.
Our new TripViewViewModel now looks like this:
[code language="obj-c"]
class TripViewViewModel {
let date: String
let departure: String
let arrival: String
let duration: String
private static let durationShortFormatter: NSDateComponentsFormatter = {
let durationFormatter = NSDateComponentsFormatter()
durationFormatter.allowedUnits = [.Hour, .Minute]
durationFormatter.unitsStyle = .Short
return durationFormatter
}()
private static let durationFullFormatter: NSDateComponentsFormatter = {
let durationFormatter = NSDateComponentsFormatter()
durationFormatter.allowedUnits = [.Hour, .Minute]
durationFormatter.unitsStyle = .Full
return durationFormatter
}()
let delay: Dynamic<String?>
let delayed: Dynamic<Bool>
var trip: Trip
init( trip: Trip) {
self.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)
duration = TripViewViewModel.durationShortFormatter.stringFromTimeInterval(trip.duration)!
delay = Dynamic(trip.delayString)
delayed = Dynamic(trip.delayed)
}
func changeActualDeparture(delta: NSTimeInterval) {
trip.actualDeparture = NSDate(timeInterval: delta, sinceDate: trip.actualDeparture)
self.delay.value = trip.delayString
self.delayed.value = trip.delayed
}
}
extension Trip {
private var delayString: String? {
return delayed ? String.localizedStringWithFormat(NSLocalizedString("%@ delay", comment: "Show the delay"), TripViewViewModel.durationFullFormatter.stringFromTimeInterval(delay)!) : nil
}
}
[/code]
Using the changeActualDeparture method we can increase or decrease the time of trip.actualDeparture. Since the delay and delayed properties on trip are now computed properties their returned values will be updated as well. We use them to set new values on the Dynamic delay and delayed properties of our TripViewViewModel. Also the logic to format the delay String has moved into an extension on Trip to avoid duplication of code.
All we have to do now to get this working again is to create bindings in the TripPresentationControl:
[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 tripModel: TripViewViewModel! {
didSet {
dateLabel.text = tripModel.date
departureTimeLabel.text = tripModel.departure
arrivalTimeLabel.text = tripModel.arrival
durationLabel.text = tripModel.arrival
tripModel.delay.bindAndFire { [unowned self] in
self.delayLabel.text = $0
}
tripModel.delayed.bindAndFire { [unowned self] delayed in
self.delayLabel.hidden = !delayed
self.departureTimeLabel.textColor = delayed ? .redColor() : UIColor(red: 0, green: 0, blue: 0.4, alpha: 1.0)
}
}
}
}
[/code]
Even though everything compiles again, we’re not done yet. We still need a way to change the delay. We’ll do that through some simple user interaction and add two buttons to our view. One to increase the delay with one minute and one to decrease it. Handling of the button taps goes into the normal view controller since we don’t want to make our Presentation Control responsible for user interaction. Our final view controller now looks like as follows:
[code language="obj-c"]
class ViewController: UIViewController {
@IBOutlet var tripPresentationControl: TripPresentationControl!
let tripModel = TripViewViewModel(Trip(departure: NSDate(timeIntervalSince1970: 1444396193), arrival: NSDate(timeIntervalSince1970: 1444397193), actualDeparture: NSDate(timeIntervalSince1970: 1444396493)))
override func viewDidLoad() {
super.viewDidLoad()
tripPresentationControl.tripModel = tripModel
}
@IBAction func increaseDelay(sender: AnyObject) {
tripModel.changeActualDeparture(60)
}
@IBAction func decreaseDelay(sender: AnyObject) {
tripModel.changeActualDeparture(-60)
}
}
[/code]
We now have an elegant way of updating the view when we tap the button. Our view controller communicates a change logical change of the model to the TripViewViewModel which in turn notifies the TripPresentationControl about a change of data, which in turn updates the UI. This way the Presentation Control doesn’t need to know anything about user interaction and our view controller doesn’t need to know about which UI components it needs to change after user interaction.
And the result:
Hopefully this post will give you a better understanding about how to use Presentation Controls and MVVM. As I mentioned in my previous post, I recommend you to read Introduction to MVVM by Ash Furrow and From MVC to MVVM in Swift by Srdan Rasic as well as his follow up post mentioned at the beginning of this post.
And of course 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.