One of the cool features of Swift are property observers, perhaps better known as the willSet and didSet. Everyone programming in Swift must have used them. Some people more than others. And some people might use them a little bit too much, changing many of them together (me sometimes included). But it’s not always completely obvious when they are called. Especially when dealing with struct, because structs can be a bit odd. Let’s dive into some situations and see what happens.
Assignment
The most obvious situation in which didSet (and willSet) gets called is by simply assigning a variable. Imagine the following struct:
struct Person { var name: String var age: Int }
And some other code, like a view controller that is using it in a variable with a property observer.
class MyViewController: UIViewController { var person: Person! { didSet { print("Person got set to '(person.name)' with age (person.age)") } } override func viewDidLoad() { person = Person(name: "Bob", age: 20) } }
As you would expect, the code in didSet will be executed when the view did load and the following is printed to the console:
Person got set to 'Bob' with age 20
Initialization
This one is also pretty clear and you probably already know about it: property observers are not executed then the variable is assigned during initialization.
var person = Person (name: "Bob", age: 20) { didSet { print("Person got set to '(person.name)' with age (person.age)") } }
This doesn’t print anything to the console. Also if you would assign person in the init function instead the willSet will not get called.
Modifying structs
A thing less known is that property observers are also executed when you change the member values of structs without (re)assigning the entire struct. The following sample illustrates this.
class ViewController: UIViewController { var person = Person (name: "Bob", age: 20) { didSet { print("Person got set to '(person.name)' with age (person.age)") } } override func viewDidLoad() { person.name = "Mike" person.age = 30 } }
In this sample we never reassign the person in our viewDidLoad function but by changing the name and age, the willSet still gets executed twice and we get as output:
Person got set to 'Mike' with age 20 Person got set to 'Mike' with age 30
Mutating functions
The same that applies to changing values of a struct also applies to mutating struct functions. Calling such a function always results in the property observers being called once. It does not matter if you replace the entire struct (by assigning self), change multiple member values or don’t change anything at all.
struct Person { var name: String var age: Int mutating func incrementAge() { if age < 100 { age++ } } }
Here we added a mutating function that increments the age as long as the age is lower than 100.
class ViewController: UIViewController { var person = Person (name: "Bob", age: 98) { didSet { print("Person got set to '(person.name)' with age (person.age)") } } override func viewDidLoad() { person.incrementAge() person.incrementAge() person.incrementAge() person.incrementAge() } }
Our willSet is called 4 times, even though the last two times nothing has changed.
Person got set to 'Bob' with age 99 Person got set to 'Bob' with age 100 Person got set to 'Bob' with age 100 Person got set to 'Bob' with age 100
Changes inside property observers
It is also possible to make changes to the variable inside its own property observers. You can reassign the entire variable, change its values or call mutating functions on it. When you do that from inside a property observer, the property observers do not trigger since that would most likely cause an endless loop. Keep in mind that changing something in willSet will be without effect since your change will be overwritten by that value that was being set originally (this gives a nice warning in Xcode as well).
class ViewController: UIViewController { var person = Person (name: "Bob", age: 98) { didSet { print("Person got set to '(person.name)' with age (person.age)") if person.name != oldValue.name { person.age = 0 print("Person '(person.name)' age has been set to 0") } } } override func viewDidLoad() { person.name = "Mike" } }
Why it matters
So why does all this matter so much you might think. Well, you might have to rethink what kind of logic you put into your property observers and which you put outside. And all this applies to Arrays and Dictionaries as well, because they are also structs. Let’s say you have an array of numbers that can change and each time it changes you want to update your UI. But you also want to sort the numbers. The following code might look fine to you at first:
class ViewController: UIViewController { var numbers: [Int] = [] { didSet { updateUI() } } override func viewDidLoad() { refreshNumbers() } func refreshNumbers() { numbers = [random() % 10, random() % 10, random() % 10, random() % 10] numbers.sortInPlace() } func updateUI() { print("UI: (numbers)") } }
Every time numbers changes, the UI will be updated. But since sortInPlace will also trigger the property observer, the UI gets updated twice:
UI: [3, 6, 7, 5] UI: [3, 5, 6, 7]
So we should really put sortInPlace inside willSet right before we call updateUI.