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:
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.
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
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.
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 } } } }
Leave a Reply