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:
willSet
- Called just before the value is storeddidSet
- Called immediately after the new value is stored
Here's the basic syntax:
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:
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:
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:
- You need to respond to property changes but don't need to control whether the changes are allowed
- You want to update other related properties or trigger UI updates
- 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:
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:
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:
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:
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:
- Create a
BankAccount
structure with abalance
property that logs all deposits and withdrawals. - Implement a
ProgressTracker
structure with apercentComplete
property that ensures the value stays between 0 and 100. - Create a
CartItem
structure withquantity
andprice
properties, and atotalPrice
property that updates automatically when either changes. - Build a
Thermostat
structure that prevents temperature settings outside a valid range. - 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! :)