Using Observers and Delegates on the Model

Model view controller can sometimes seem difficult. You have something to do that needs the controller, but has no good way to get there.  One common example is refreshing a display. We change data , but how do we tell the controller when there is fresh data to update the view. Often, we add a method for display refresh everywhere we change the data in the view controller. This can get messy. Another way is using notifications with  key-value. There is a third in Swift called property observers which may provide an easier solution to those types of problems.

In this lesson, we’ll use property observers and delegates to refresh data in the view.

Set up the project

Start a new single view  Xcode Project called PropertyObserverDemo. Set Swift for the language and a Universal  device.   Go to the story board and add six labels a switch and a stepper. Arrange them like this:

2016-08-01_07-11-48

For the Ice Cream Label, using text styles set the font to Title 1.

2016-08-01_05-54-31

Set the font for Ice cream description to Headline. Set the Font for Price 000.00 to Title 2.

For the stepper set the attributes as follows:

2016-08-01_05-55-18

Change  the  ViewController  class to this:

class ViewController: UIViewController {
    @IBOutlet weak var iceCreamDescription: UILabel!
    @IBOutlet weak var iceCreamPriceLabel: UILabel!
    @IBAction func didChangeScoops(_ sender: UIStepper) {
    }
    @IBAction func didChooseCone(_ sender: UISwitch) {
    }
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Connect IceCreamDescription to the Ice cream description Label by dragging from the circle to the label. Connect the  iceCreamPriceLabel to the Price 000.00 label the same way. Connect didChangeScoops to the stepper and didChooseCone to the switch.

Add a Model

Let’s add simple model to this. Press Command-N and add the following model named IceCream subclassing NSObject to the project:

class IceCream: NSObject {
    let pricePerScoop = 2.5
    var scoops =  0
    var isInCone = true
    var price:Double {
        get{
            let iceCreamPrice = Double(scoops) * pricePerScoop
            if isInCone {
                return iceCreamPrice + 0.25
            } else{
                return iceCreamPrice + 0.10
            }
        }
    }
}

This has three properties and a constant.  scoops and isInCone are properties, and our third property price is a computed property based on the values of scoops and isInCone.

A display method

Go back to the ViewController class. Above the outlets, add the model:

let iceCream = IceCream()

In viewDidLoad, add the following to initialize the model

override func viewDidLoad() {
    super.viewDidLoad()
    iceCream.isInCone = true
    iceCream.scoops = 1
}

Change our actions like this:

@IBAction func didChangeScoops(_ sender: UIStepper) {
    iceCream.scoops = Int(sender.value)
}
    
@IBAction func didChooseCone(_ sender: UISwitch) {
    iceCream.isInCone = sender.isOn
}

Add the following method:

func refreshDisplay(){
    var cone = "in a cone"
    if !iceCream.isInCone{
       cone = "in a dish"
    }
    iceCreamDescription.text = "\(iceCream.scoops) scoops " + cone
    iceCreamPriceLabel.text = String(
        format:"Price: %2.2f",
        iceCream.price)
}

This code is modifies the model by the stepper and switch. The refreshDisplay method changes the two labels to reflect these changes in the model. However nothing has called refreshDisplay yet.

A very common and simple way to call refreshDisplay is to add it to the actions and viewDidLoad. We could do this for example to didChangeScoops:

@IBAction func didChangeScoops(_ sender: UIStepper) {
    iceCream.scoops = Int(sender.value)
    refreshDisplay()
}

But that would require every method that changes a property of iceCream to call this method. That can be cumbersome. Another way is to have the model tell the controller there’s been a change.

Using Property Observers

In order to tell the controller, there’s two parts: knowing there is a change worth reporting and telling the controller. For knowing there’s a change we can use property observers. Property observers are additions to the property that run code when the property’s value changes. Two keywords didSet and willSet perform this. didSet executes the code in the block after the values changes and willSet before the value change. For example, you might have a property in some function like this to change from our programming index values starting with 0 to user-friendly values starting with 1:

var index = 1{
    didSet{
     index += 1
    }
}

Any time index changes, we add 1 to index. Property observes only fire after initialization. They do not fire for initializing the variable.

The model needs to tell the controller there’s been a change. This is another good use for a delegate. We can create a delegate method iceCreamDidChange with no arguments that runs in ViewController when there is a change to the properties. Almost always keep to using no arguments when using this. The only thing it does is states there is a change. Never pass values of the model. Use the model in your view controller to access the values. The only exception is an argument that tells the  view controller what changed in the model.

Add a Delegate

Go to the IceCream class. Add a protocol above it

protocol IceCreamDelegate{
    func iceCreamDidChange()
}

In IceCream, add a property delegate:

    var delegate:IceCreamDelegate? = nil

Notice I used IceCreamDelegate? not IceCreamDelegate! for the type. Usually I use IceCreamDelegate! in delegates between view controllers to force myself to set the delegate property. With IceCreamDelegate!, a nil causes a run time error. With this type of delegate, there may be times I want to shut down updating. By using IceCreamDelegate? and setting the delegate to nil in the view controller, I shut down the updating.

Add The Property Observers

Change scoops to this:

var scoops =  0{
    didSet{
        delegate?.iceCreamDidChange()
    }
}

The property observer calls the delegate method when there is a change in the property.  Now do the same to isInCone

var isInCone = true{
    didSet{
        delegate?.iceCreamDidChange()
    }
}

Depending on your application, you can set up property observers for the properties that notify the view controller and leave it off for properties that do not.

Adopt the Delegate

Go to ViewController. Adopt the delegate:

class ViewController: UIViewController,IceCreamDelegate {

Add the required method.

//MARK: Delegates
    func iceCreamDidChange() {
        refreshDisplay()
    }

This method only notifies the controller of the change. It’s up to the controller to do something about it by code in the method. In our case, we’ll refresh the display.

Finally, add the following line to viewDidLoad to locate the delegate

override func viewDidLoad() {
    super.viewDidLoad()
    iceCream.delegate = self
    iceCream.isInCone = true
    iceCream.scoops = 1
}

Set your simulator for an iPhone 6s. Build and Run

2016-08-01_07-41-38

Change the values and you’ll find the display updates correctly.

2016-08-01_07-42-44

2016-08-01_07-43-39

This is a quick method for controlling updating of models when the controller needs to. It has the advantage that any controller using the model can decide how it wants to handle the change. It has the disadvantage of hiding refreshing display code in the view controller and requiring several calls to the delegate in the model. You’ll find similar patterns to this in Apple’s API’s that provide delegates to handle events. For example UITableViewDelegate  has the tableView(_:didSelectRowAt:) delegate method for informing the view controller the user selected a row from the table view.

The Property Observer – Delegate pattern provides one way to inform your controller of events. There are other such as key:value observers, but I find for many applications this is enough to do the job.

The Whole Code

ViewController.swift

//
//  ViewController.swift
//  PropertyObserverDemo
//
//  Created by Steven Lipton on 7/31/16.
//  Copyright © 2016 Steven Lipton. All rights reserved.
//

import UIKit

class ViewController: UIViewController,IceCreamDelegate {
    let iceCream = IceCream()
    @IBOutlet weak var iceCreamDescription: UILabel!
    
    @IBOutlet weak var iceCreamPriceLabel: UILabel!
    @IBAction func didChangeScoops(_ sender: UIStepper) {
        iceCream.scoops = Int(sender.value)
    }
    
    @IBAction func didChooseCone(_ sender: UISwitch) {
        iceCream.isInCone = sender.isOn
    }
    
    func refreshDisplay(){
        var cone = "in a cone"
        if !iceCream.isInCone{
            cone = "in a dish"
        }
        iceCreamDescription.text = "\(iceCream.scoops) scoops " + cone
        iceCreamPriceLabel.text = String(format:"Price: %2.2f",iceCream.price)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        iceCream.delegate = self
        iceCream.isInCone = true
        iceCream.scoops = 1
    }

   //MARK: Delegates
    func iceCreamDidChange() {
        refreshDisplay()
    }
}

IceCream.swift

//
//  IceCream.swift
//  PropertyObserverDemo
//
//  Created by Steven Lipton on 7/31/16.
//  Copyright © 2016 Steven Lipton. All rights reserved.
//

import UIKit

protocol IceCreamDelegate{
    func iceCreamDidChange()
}

class IceCream: NSObject {
    let pricePerScoop = 2.5
    var delegate:IceCreamDelegate? = nil
    var scoops =  0{
        didSet{
            delegate?.iceCreamDidChange()
        }
    }
    var isInCone = true{
        didSet{
            delegate?.iceCreamDidChange()
        }
    }
    var price:Double {
        get{
            let iceCreamPrice = Double(scoops) * pricePerScoop
            if isInCone {
                return iceCreamPrice + 0.25
            } else{
                return iceCreamPrice + 0.10
            }
        }
    }
}

4 Replies to “Using Observers and Delegates on the Model”

  1. Hi Steven, if I may, here are some remarks :
    – most important one : you should add “weak” to your delegate property or it will create a retain cycle between your model and your viewcontroller
    – the pattern used here is delegation pattern (not observers/delegate pattern). You use observers to inform the delegate that something has changed but there is no subscription to any observable property. I’m saying that because this could create a confusion with the notion of observable or observer pattern :-)
    Some small swift tips :
    – when set is not use there is no need to indicate get in your variable :-)
    – your delegate var is by default set to nil when declared as optional (so no need for “= nil” in the declaration)
    – RayWenderlich recommends to put protocol conformance in extension
    I would probably have put pricePerScoop has a private var and would probably have used NSNumberFormatter to display the price and this price would have been a computed var of IceCream Model (placed in an extension of IceCream Model).
    Finally, I would probably have a look at RX as this would definitely simplify all the code (but that’s probably out of scope) :-)
    For the rest of it, very interesting article that I would recommend to beginners in iOS (I mentioned it to our juniors here :-))

    1. All valid points. I was trying to keep it simple, though defintely the weak is important to add. I was trying to be very careful and use Property observer to make that differentiation. This is Apple nomenclature and like Notifications confusion starts in Cupertino.

  2. Hi Steve,

    Very much enjoyed the article and I’m going to use it to hack with my own property observers. Just a couple of things:

    1) In IceCream.swift you have pricePer as opposed to pricePerScoop–this looks like a typo-but it could also be the getter recursively calling price but then you would need the prefix “self’. Unfortunately it would give you a warning and the program would stop. Don’t mean to pick a nit because the code works with pricePerScoop. Just wanted to see if there was something I was missing.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s