Training and Instructional Design
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.
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:
For the Ice Cream Label, using text styles set the font to Title 1.
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:
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.
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
.
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.
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.
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.
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.
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
Change the values and you’ll find the display updates correctly.
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.
// // 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 // 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 } } } }
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 :-))
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.
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.
Yep. A typo.
Nice article. Though, what’s the reason for not passing arguments from model to controller? What’s the advantage of letting the controller retrieve the data from the model itself? Could you elaborate please. /newbie
My apologies for missing this comment. I written about this in detail before, so I’ll just point you in the direction of one of those articles. https://makeapppie.com/2016/09/23/why-do-we-need-delegates-in-ios-and-watchos/