We’re going to learn about a feature of Swift called “property observers” that help developers manage app state. You can easily add code to monitor changes to Swift native type property values as well as your own custom type property values. Remember that you can gain insight into an application by looking at its state: the data values stored in all properties of the app at a specific point in time. Getting a grip on app state, therefore managing complexity, is one of the biggest challenges in computer science. Property observers are one technology that help you get a grip. In today’s article, I’ll explain this Swift feature, demonstrate its usage with runnable examples of Swift code, show you how I built an app which relies on property observers, and provide you a list of other Swift technologies that help you manage app state and complexity. Here’s my sample app in action:
Download the Xcode 9 project and playground, both written in Swift 4, from GitHub so you can follow along with my discussion.
Introduction
Apps store data in variables. Often, though not exclusively, app data is provided by users of the app, whether human or not, as “input.” Remember there are apps that, say, help people manage their bank accounts and there are embedded apps that monitor the activities of, say, automobile engines.
In computer science, the contents of these variables at any moment in time during the app’s lifetime, i.e., while the app is executing, determine the app’s state. If you could make a list of all an app’s data at a given point in time, you’d have a snapshot roughly describing what is happening in the app at that moment.
I use the term “roughly” because, computer science definitions or not, an app’s state is more than just a list of its data at a point in time. The context in which the app is running — call it the app’s “configuration” — must also be taken into account.
Think about a word processing application. Because of multiprocessors and threaded code, the word processor may be doing a number of complex tasks at the same time: the user is dictating text into a new document using a Bluetooth headset; sometimes the user types text with the keyboard (input is not exclusively through the Bluetooth headset); text is being constantly checked for syntax and grammatical errors; the stock image/shapes library was updated by the vendor so the app is now downloading a set of files over the Internet; the social media plugin is monitoring for new likes and tweets on articles the user has previously written and posted using the app; etc.
I think you get the point. A “simple” word processing app is not so simple. In fact, defining all the possible states that such an application can take on is computationally impossible. Most applications nowadays fall into this not-so-simple category. They can enter stochastic (random) states no matter how hard developers try to manage complexity. (I love quoting my own publications, as in the link for the term “stochastic (random) states.”)
Controlling complexity
Properties
Remember that in the simplest terms, Swift classes are made up of properties and methods, or what humans would call data and behaviors, respectively. This data contains information about the state of your application. Many of you have written applications that store, at least temporarily, pieces of data like a person’s username, a user’s account number, the current temperature in a user’s neighborhood, how many items are in a user’s shopping cart, the gross value in dollars of their shopping cart, etc.
Monitoring property state
Did you know that Swift can notify you when when a property (member variable) of one of your classes or structs 1) will be changed and 2) has changed? Swift gives you the opportunity to manage your application’s state. You have to write a little bit of code, but it’s intuitive and the syntax is very readable, thus showing again how expressive and powerful the Swift language can be. Apple calls this property state monitoring aspect of the Swift language “property observers.” Today, I’ll explain how property observers work, and discuss when you should use them, how you shouldn’t abuse them, and show you plenty of example code to get you up and running with this elegant and straightforward feature of the Swift language. To get you down in the trenches, I’ll build a fully-functional temperature conversion app which allows users to transform Fahrenheit values to Celsius values and Celsius values to Fahrenheit values.
Property observers to the rescue
Code complexity has to be controlled with a combination of features. I’ll summarize many of the tools available later. Today, we’ll concentrate on Swift property observers.
Harmony starts at home. By that I mean that you can’t conquer an entire codebase’s complexity at once. You need to divide and conquer. The best app architectures are the ones in which designers and developers solve problems by creating solutions made up of relatively small components that interact using well-defined interfaces. Classes are a great example of such small components that interact using well-defined interfaces. If you’ve been reading my blog, you know by now that I like to use small and well-defined classes.
By adding property observers to your classes judiciously, you can start controlling complexity as you build your solution.
You apply property observers to class member variables (properties). These observers inform you every time a property is about to be changed and when the property is actually changed. This is your chance to control complexity. Think about it: You can get automatically notified when the property or properties of a class instance (object) will and have changed. You’re getting notified of state changes.
Definitions
According to Apple’s Swift 4.1 documentation:
Property observers observe and respond to changes in a property’s value. Property observers are called every time a property’s value is set, even if the new value is the same as the property’s current value. …
You have the option to define either or both of these observers on a property:
willSet
is called just before the value is stored.didSet
is called immediately after the new value is stored.
If you implement a willSet
observer, it’s passed the new property value as a constant parameter. You can specify a name for this parameter as part of your willSet
implementation. If you don’t write the parameter name and parentheses within your implementation, the parameter is made available with a default parameter name of newValue
.
Similarly, if you implement a didSet
observer, it’s passed a constant parameter containing the old property value. You can name the parameter or use the default parameter name of oldValue
. If you assign a value to a property within its own didSet
observer, the new value that you assign replaces the one that was just set. …
Simple example of property observers
I’ve written some simple examples of property observers so you can understand their basic functionality. Let start with a struct
in which I accept the the default parameter names of oldValue
and newValue
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
struct Human { var name:String { willSet // "name" is ABOUT TO CHANGE { print("\nName WILL be set...") print("from current value: \(name)") print("to new value: \(newValue).\n") } didSet // "name" HAS CHANGED { print("Name WAS changed...") print("from current value: \(oldValue)") print("to new value: \(name).\n") } } } var person = Human(name: "John") person.name = "Jack" |
Here’s the output from line 21:
1 2 3 4 5 6 7 |
Name WILL be set... from current value: John to new value: Jack. Name WAS changed... from current value: John to new value: Jack. |
Now I’ll use property observers with a class
. Instead of accepting the default parameter names of oldValue
and newValue
, I’ll define my own — in parentheses immediately following the willSet
and didSet
observer declarations. Notice that, since the name
variable is not optional, I had to add an initializer (init
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Being { var name:String { willSet(toNewName) { print("\nName WILL be set...") print("from current value: \(name)") print("to new value: \(toNewName).\n") } didSet(fromOldName) { print("Name WAS changed...") print("from current value: \(fromOldName)") print("to new value: \(name).\n") } } init(name:String) { self.name = name } } var alien:Being = Being(name: "Mary") alien.name = "Marge" |
Here’s the output from line 25:
1 2 3 4 5 6 7 |
Name WILL be set... from current value: Mary to new value: Marge. Name WAS changed... from current value: Mary to new value: Marge. |
Intermediate conclusions
After reviewing the previous two property observer examples, you should have a cursory understanding of how they work. Let me make a few observations after modifying the class named “Being:”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class Being { var name:String { willSet(toNewName) { print("\nName WILL be set...") print("from current value: \(name)") print("to new value: \(toNewName).\n") name = "Placeholder" } didSet(fromOldName) { print("Name WAS changed...") print("from current value: \(fromOldName)") print("to new value: \(name).\n") name = "Placeholder" } } init(name:String) { self.name = name } } var alien:Being = Being(name: "Mary") alien.name = "Marge" print("Name CHANGED in didSet: \(alien.name)") |
Here’s the output from lines 27 and 28:
1 2 3 4 5 6 7 8 9 |
Name WILL be set... from current value: Mary to new value: Marge. Name WAS changed... from current value: Mary to new value: Marge. Name CHANGED in didSet: Placeholder |
As to the intermediate conclusions… Notice that I was able to change the value of a property in a property observer (in didSet
). I sometimes wonder if that’s good practice, but I can envision a use case in which changing a property in it’s own observer is legitimate, like rounding a number to a specific number of digits, or perhaps adding a prefix like “Mrs. ” or “Mr. ” to a person’s name.
Notice that trying to change a property’s value in willSet
does not work, and Xcode will catch that scenario and warn you with the message: “Attempting to store to property ‘name’ within its own willSet, which is about to be overwritten by the new value.” Isn’t that self-explanatory? See this image:
When I first learned about property observers, I had the notion that they would be limited to atomic operations and simple operations. They’re not. I can do pretty much anything in a property observer, from changing the observed property’s value to spinning off a new thread:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class Being { var name:String { willSet(toNewName) { print("\nName WILL be set...") print("from current value: \(name)") print("to new value: \(toNewName).\n") // name = "Placeholder" DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async { for index in 1...1000000000 { print("\(index) times 1000 is \(index * 1000)") } DispatchQueue.main.async { print("Done with useless loop") } } } didSet(fromOldName) { print("Name WAS changed...") print("from current value: \(fromOldName)") print("to new value: \(name).\n") // name = "Placeholder" } } init(name:String) { self.name = name } } |
You can even update the user interface (UI) from a property observer. Why am I bringing this up?
I see property observers as a very powerful and elegant tool that should be used only for the right reasons and/or only when necessary.
My mantra is “keep it simple and clean” (Occam’s razor). My advice to you is to use property observers for simple tasks like:
- managing the state of a single property as we’ll see in my sample app below (or on GitHub), and,
- keeping an audit trail of changes to a value.
By “managing state,” I’m referring to scenarios like performing mathematical calculations or string manipulations directly related to the property and/or its companion properties.
Doing things like updating the UI directly from willSet
or didSet
in an app’s classes for core business logic is asking for trouble. Your code for app logic and your code for UI will become too tightly coupled and you’d be breaking the Model-View-Controller (MVC) design pattern. If you feel you absolutely must update the UI from a property observer, use something like messaging via NSNotificationCenter
. I discuss NSNotificationCenter
, loose coupling, and tight coupling in my article at this link.
I’m throwing a lot at you in this article, so I don’t want to get caught up in too many details. There is one last thing I should mention: how and the order in which property observers are called when inheritance is involved. I leave it to you to read Apple’s discussion on this subject.
Property observers support custom types
Property observers don’t only apply to Swift’s built-in types like Int
, Float
, and Bool
. You can define property observers for your own custom types.
You’ll notice in my sample app that I only call a method from my property observers if the property is being changed. In order to determine if a property is being assigned a new value, you need to use the !=
operator. What do you have to do to make your custom type support the !=
operator? Do you remember? Did you read my article on Swift generics?
If you don’t remember the answer to the question “What do you have to do to make your custom type support the !=
operator?,” the answer is to make your custom type conform to the Equatable
protocol:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
// // CUSTOM CLASS // class Person : Equatable { var name:String var weight:Int var sex:String init(weight:Int, name:String, sex:String) { self.name = name self.weight = weight self.sex = sex } // Conformance to Equatable. static func == (lhs: Person, rhs: Person) -> Bool { if lhs.weight == rhs.weight && lhs.name == rhs.name && lhs.sex == rhs.sex { return true } else { return false } } } // end class Person class Hierarchy { // // Property observer on a // COMPLEX/CUSTOM type. // var leader:Person { willSet { // // NO COMPARISONS POSSIBLE IF // Person CLASS DOES NOT CONFORM // TO Equatable. // if newValue != leader { print("\nThere will be a change in leadership.") print("\(newValue.name) will be the new leader.") } } didSet { // // NO COMPARISONS POSSIBLE IF // Person CLASS DOES NOT CONFORM // TO Equatable. // if oldValue != leader { print("\nThere was a change in leadership.") print("\(oldValue.name) was replaced.\n") } } } var people:[Person] init(withLeader:Person) { self.leader = withLeader self.people = [Person]() self.people.append(leader) } } // end class Hierarchy let frank = Person(weight: 180, name: "Frank", sex: "M") let brigade = Hierarchy(withLeader: frank) let barbara = Person(weight: 120, name: "Barbara", sex: "F") brigade.leader = barbara |
Here’s the output from line 85:
1 2 3 4 5 |
There will be a change in leadership. Barbara will be the new leader. There was a change in leadership. Frank was replaced. |
Sample app demonstrating property observers
In order to demonstrate property observers, I’ve designed and developed a fully-functional temperature conversion app (Fahrenheit to Celsius and Celsius to Fahrenheit). Given the rather lengthy discussion leading up to this point, I won’t explain the app’s commonplace structural details, i.e., storyboard, IBOutlets, IBActions, etc. Given the fact that the app is based on the Xcode 9 Single View App
project template, most of you know how to build this sample app. First I’ll discuss how property observers are used in the app and then I’ll show you the most important app code. After reading this section and the preceding discussion, I would hope that you have gained a basic grasp of property observers. Download the Xcode 9 project and playground, both written in Swift 4, from GitHub so you can follow along with my discussion.
This app makes what I consider to be common sense and legitimate use of property observers for managing the state of the app’s core business logic class, Temperature
. Remember that by “managing state,” I’m referring to scenarios like performing mathematical calculations or string manipulations directly related to a property and/or its companion properties.
I making sure that the state of my Temperature
class’s member properties, fahrenheit
and celsius
are always “synchronized.” Since people all over the world use both temperature types, I want to always be able to provide the currently stored temperature in both fahrenheit and celsius. Since temperature fluctuates over time, a user of my class would continually be updating they’re preferred temperature type property. No matter which temperature type property they update, either fahrenheit
and celsius
, the other type will be updated with its corresponding, respective, and properly-converted value. The state of my class’s properties is guaranteed to be correct/accurate because of property observers. And no, I haven’t tested it exhaustively for every possible contingency. This is a sample app. Please read the licensing agreement under which you must use the app code and project.
Note that while I generally dislike the idea of using a property observer to update the UI, I’ve done so in the sample app using a loosely-coupled solution, namely, messaging via NSNotificationCenter
. See my article on writing loosely coupled code using NSNotificationCenter
.
Here’s how the app works:
Here’s the core business logic class in file Temperature.swift
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
import Foundation import UIKit enum TemperatureType { case Fahrenheit case Celsius } class Temperature { var fahrenheit:Float { willSet { if fahrenheit != newValue { print("fahrenheit temperature will change") } } didSet { if fahrenheit != oldValue { print("fahrenheit temperature did change") // when fahrenheit changed, always get celsius equivalent celsius = Celsius() // when fahrenheit changed, broadcast change to // any listeners if fahrenheit <= 32.0 { NotificationCenter.default.post(name: Notification.Name(rawValue: temperatureIsFreezingNotificationID), object: self) } } } } // end var fahrenheit:Float var celsius:Float { willSet(newCelsiusValue) { if celsius != newCelsiusValue { print("celsius temperature will change") } } didSet { if celsius != oldValue { print("celsius temperature did change") // when celsius changed, always get fahrenheit equivalent fahrenheit = Fahrenheit() // when celsius changed, broadcast change to // any listeners if celsius <= 0.0 { NotificationCenter.default.post(name: Notification.Name(rawValue: temperatureIsFreezingNotificationID), object: self) } } } } // end var celsius:Float init() { fahrenheit = 32.0 celsius = 0.0 } init(temperature:Float, of type:TemperatureType) { if type == TemperatureType.Fahrenheit { fahrenheit = temperature celsius = (temperature - 32) / 1.8 // NOTE: we CANNOT call instance method // Celsius() in an initializer } else if type == TemperatureType.Celsius { celsius = temperature fahrenheit = (temperature * 1.8) + 32 // NOTE: we CANNOT call instance method // Fahrenheit() in an initializer } else { celsius = -1000000.0 fahrenheit = -1000000.0 } } // Convert the currently-stored temperature to fahrenheit. // // - returns: Float: The current temperature in fahrenheit. func Fahrenheit() -> Float { return (celsius * 1.8) + 32 } // Convert the currently-stored temperature to celsius. // // - returns: Float: The current temperature in celsius. func Celsius() -> Float { return (fahrenheit - 32) / 1.8 } func currently() { let info = """ Fahrenheit: \(fahrenheit) Celsius: \(celsius)\n """ print(info) } } // end class Temperature |
Here’s the one and only UIViewController in file ViewController.swift
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
import UIKit let temperatureIsFreezingNotificationID = "com.iosbrain.temperatureIsFreezingNotificationID" class ViewController: UIViewController { @IBOutlet weak var temperatureView: UIView! @IBOutlet weak var convertedTemperatureLabel: UILabel! @IBOutlet weak var convertCtoFText: UITextField! @IBOutlet weak var convertFtoCText: UITextField! var color = UIColor.blue var temperature = Temperature(temperature: 70.0, of: TemperatureType.Fahrenheit) override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. convertedTemperatureLabel.text = "" NotificationCenter.default.addObserver(self, selector: #selector(didTemperatureReachFreezing), name: Notification.Name(rawValue: temperatureIsFreezingNotificationID), object: nil) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } deinit { NotificationCenter.default.removeObserver(self, name: Notification.Name(rawValue: temperatureIsFreezingNotificationID), object: nil) } @IBAction func convertCtoFButtonTapped(_ sender: Any) { if convertCtoFText.text != "" { temperatureView.backgroundColor = UIColor.green convertFtoCText.text = "" let celsiusValue = (convertCtoFText.text as NSString?)?.floatValue let temperature = Temperature(temperature: celsiusValue!, of: TemperatureType.Celsius) convertedTemperatureLabel.text = String(temperature.fahrenheit) + "°F" self.temperature.fahrenheit = temperature.fahrenheit } else { convertedTemperatureLabel.text = "" convertFtoCText.text = "" } } @IBAction func convertFtoCButtonTapped(_ sender: Any) { if convertFtoCText.text != "" { temperatureView.backgroundColor = UIColor.green convertCtoFText.text = "" let fahrenheitValue = (convertFtoCText.text as NSString?)?.floatValue let temperature = Temperature(temperature: fahrenheitValue!, of: TemperatureType.Fahrenheit) convertedTemperatureLabel.text = String(temperature.celsius) + "°C" self.temperature.celsius = temperature.celsius } else { convertedTemperatureLabel.text = "" convertCtoFText.text = "" } } @objc func didTemperatureReachFreezing() { temperatureView.backgroundColor = UIColor.blue } } // end class ViewController |
Wrapping up
I hope y’all enjoyed the discussion of property observers. Please remember to use them judiciously and not overuse or abuse them. Property observers are not the only Swift feature that help in managing app state and complexity. Here’s a list of other Swift technologies covered on this website that also help you tackle state and complexity:
- object-oriented language features like inheritance and polymorphism;
- breaking large pieces of code into smaller ones with well-defined interfaces (like with Swift extensions);
- using design patterns like delegation;
- using interfaces/protocols; and,
- writing expressive, meaningful, and English-like code.
Above all, remember to enjoy life, not just work!