Skip to main content

Swift Property Observers

Introduction

Property observers are a powerful feature in Swift that let you monitor and respond to changes in a property's value. They provide a way to execute custom code whenever a property is about to be set (using willSet) or has just been set (using didSet). This allows you to keep your code modular and maintain clean separation of concerns, particularly when developing complex applications.

Property observers are especially useful when you need to:

  • Update a user interface when data changes
  • Validate new values as they're assigned
  • Sync changes with external systems
  • Log changes for debugging purposes

In this tutorial, we'll explore how property observers work, when to use them, and see practical examples of their implementation in real-world scenarios.

Basic Syntax of Property Observers

Swift provides two types of property observers:

  1. willSet - Called just before the value is stored
  2. didSet - Called immediately after the new value is stored

Here's the basic syntax:

swift
var propertyName: Type = initialValue {
willSet(newValue) {
// Code to be executed before property is changed
// newValue contains the incoming value
}
didSet(oldValue) {
// Code to be executed after property is changed
// oldValue contains the previous value
}
}

The parameter names newValue and oldValue are optional. If you don't provide them, Swift automatically provides them with these default names.

Simple Property Observer Example

Let's start with a basic example to see property observers in action:

swift
var steps: Int = 0 {
willSet {
print("About to set steps to \(newValue)")
}
didSet {
print("Added \(steps - oldValue) steps")
print("Steps are now \(steps)")
}
}

steps = 100

Output:

About to set steps to 100
Added 100 steps
Steps are now 100

When we set steps to 100, the willSet observer is called first, printing the upcoming new value. After the value is set, the didSet observer is called, which can access both the new value (via the property name) and the old value (via oldValue).

Using Property Observers in Structures

Property observers are particularly useful within structures. Let's create a StepCounter structure:

swift
struct StepCounter {
var totalSteps: Int = 0 {
willSet {
print("About to set totalSteps to \(newValue)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
} else if totalSteps < oldValue {
print("Removed \(oldValue - totalSteps) steps")
}
}
}
}

var stepCounter = StepCounter()
stepCounter.totalSteps = 200
stepCounter.totalSteps = 360
stepCounter.totalSteps = 300

Output:

About to set totalSteps to 200
Added 200 steps
About to set totalSteps to 360
Added 160 steps
About to set totalSteps to 300
Removed 60 steps

This example demonstrates how property observers can help track changes and perform different actions based on how the value changes.

When to Use Property Observers

Property observers are best used when:

  1. You need to respond to property changes but don't need to control whether the changes are allowed
  2. You want to update other related properties or trigger UI updates
  3. You need to maintain consistency between properties

If you need to validate or potentially reject a new value before it's stored, computed properties with custom getters and setters are often a better choice than property observers.

Practical Example: Temperature Converter

Here's a more practical example - a temperature converter that automatically converts between Celsius and Fahrenheit:

swift
struct TemperatureConverter {
var celsius: Double = 0.0 {
didSet {
fahrenheit = (celsius * 9/5) + 32
}
}

var fahrenheit: Double = 32.0 {
didSet {
celsius = (fahrenheit - 32) * 5/9
}
}
}

var temp = TemperatureConverter()
print("Initial: \(temp.celsius)°C, \(temp.fahrenheit)°F")

temp.celsius = 25
print("After setting Celsius: \(temp.celsius)°C, \(temp.fahrenheit)°F")

temp.fahrenheit = 68
print("After setting Fahrenheit: \(temp.celsius)°C, \(temp.fahrenheit)°F")

Output:

Initial: 0.0°C, 32.0°F
After setting Celsius: 25.0°C, 77.0°F
After setting Fahrenheit: 20.0°C, 68.0°F

This example demonstrates bidirectional synchronization between properties. When one property changes, the other updates automatically to maintain consistency.

Property Observers with Initialization

An important thing to note is that property observers are not called during initialization. They only trigger when the property is changed after the instance is fully initialized:

swift
struct User {
var name: String {
didSet {
print("Name changed to \(name)")
}
}

init(name: String) {
self.name = name // didSet won't be called here
print("Initialized with name: \(name)")
}
}

var user = User(name: "John")
user.name = "Jane"

Output:

Initialized with name: John
Name changed to Jane

Notice that the didSet observer only triggered when we changed the name after initialization, not during initialization itself.

Real-World Application: Form Validation

Let's see how property observers can be used to implement real-time form validation:

swift
struct RegistrationForm {
var username: String = "" {
didSet {
validateUsername()
}
}

var password: String = "" {
didSet {
validatePassword()
}
}

var usernameStatus: String = ""
var passwordStatus: String = ""
var isValid: Bool = false {
didSet {
if isValid {
print("Form is now valid, submit button enabled")
} else {
print("Form is invalid, submit button disabled")
}
}
}

private mutating func validateUsername() {
if username.isEmpty {
usernameStatus = "Username cannot be empty"
isValid = false
} else if username.count < 4 {
usernameStatus = "Username must be at least 4 characters"
isValid = false
} else {
usernameStatus = "Username is valid"
validateForm()
}
print("Username status: \(usernameStatus)")
}

private mutating func validatePassword() {
if password.isEmpty {
passwordStatus = "Password cannot be empty"
isValid = false
} else if password.count < 8 {
passwordStatus = "Password must be at least 8 characters"
isValid = false
} else {
passwordStatus = "Password is valid"
validateForm()
}
print("Password status: \(passwordStatus)")
}

private mutating func validateForm() {
// Only set to true if both fields are valid
isValid = !username.isEmpty && username.count >= 4 &&
!password.isEmpty && password.count >= 8
}
}

var form = RegistrationForm()
form.username = "joe"
form.password = "weakpw"
form.username = "joseph"
form.password = "strongpassword123"

Output:

Username status: Username must be at least 4 characters
Form is invalid, submit button disabled
Password status: Password must be at least 8 characters
Form is invalid, submit button disabled
Username status: Username is valid
Form is invalid, submit button disabled
Password status: Password is valid
Form is now valid, submit button enabled

This example shows how property observers can create a responsive form validation system that updates validation status immediately as the user enters information.

Advanced: Avoiding Observer Recursion

When using property observers that modify other properties, you need to be careful to avoid infinite recursion. Here's an example of a potential pitfall and how to avoid it:

swift
struct Circle {
var radius: Double = 0 {
didSet {
// If we change diameter here, it would trigger radius didSet again!
print("Radius changed to \(radius)")
diameter = radius * 2
}
}

var diameter: Double = 0 {
didSet {
// This would change radius, which would change diameter, etc.
print("Diameter changed to \(diameter)")

// To prevent recursion, we can check if the value is really different
let expectedRadius = diameter / 2
if radius != expectedRadius {
radius = expectedRadius
}
}
}
}

var circle = Circle()
circle.radius = 5
circle.diameter = 14

Output:

Radius changed to 5
Diameter changed to 10
Diameter changed to 14
Radius changed to 7
Diameter changed to 14

Notice how we check if the value is really different before making a change to prevent infinite recursion.

Summary

Property observers are a powerful Swift feature that allow you to:

  • Monitor changes to property values
  • Respond to those changes with custom code
  • Execute code before (willSet) or after (didSet) a property value changes
  • Access both new and old values during the change process
  • Build responsive, self-updating data models

They're especially useful for maintaining property consistency, updating UI elements in response to data changes, and implementing validation systems. By using property observers effectively, you can write cleaner, more responsive code with better separation of concerns.

Exercises

To solidify your understanding of property observers, try these exercises:

  1. Create a BankAccount structure with a balance property that logs all deposits and withdrawals.
  2. Implement a ProgressTracker structure with a percentComplete property that ensures the value stays between 0 and 100.
  3. Create a CartItem structure with quantity and price properties, and a totalPrice property that updates automatically when either changes.
  4. Build a Thermostat structure that prevents temperature settings outside a valid range.
  5. Implement a ColorRGB structure with properties for red, green, and blue components (0-255) and a property that converts to hexadecimal format whenever a component changes.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)