Blog / September 21, 2024 / 8 mins read / By Mahi Garg

Property Observers (WillSet & DidSet) in Swift

Property observers in Swift are an incredibly useful feature that allows you to monitor and respond to changes in a property’s value. They are especially handy when you want to perform additional tasks whenever a property is set or updated, such as updating the UI, logging data, or enforcing business logic.

In this blog, we’ll dive into what property observers are, how they work, and how to use them in your Swift code with practical examples.

What are Property Observers?

Property observers observe and respond to changes in a property’s value. You can define property observers for both stored properties (properties with an initial value) and computed properties. Property observers do not trigger for the initial assignment (when the property is first initialized); they only react when the property’s value is explicitly set.

Swift provides two kinds of property observers:

  • willSet: Called just before the value is set.
  • didSet: Called immediately after the value has been set.

Syntax for Property Observers

Here is the basic syntax:

var property: Type {
    willSet {
        // Code executed just before the property is set
    }
    didSet {
        // Code executed immediately after the property is set
    }
}

Let’s now explore these observers with examples.

Using didSet to Track Property Changes

Suppose we have a class that represents a UserProfile where we want to keep track of the user’s age. We can use the didSet observer to react whenever the age property changes:

class UserProfile {
    var age: Int = 0 {
        didSet {
            print("User's age changed from \(oldValue) to \(age)")
        }
    }
}

let profile = UserProfile()
profile.age = 25   // Output: User's age changed from 0 to 25
profile.age = 30   // Output: User's age changed from 25 to 30

In the above example:

The didSet observer is called every time the age property is updated. The oldValue constant holds the property’s previous value.

Using willSet to Perform Actions Before Changing a Property

Let’s modify the previous example to use the willSet observer. This will allow us to perform some action just before the property is updated.

class UserProfile {
    var age: Int = 0 {
        willSet {
            print("About to change user's age from \(age) to \(newValue)")
        }
    }
}

let profile = UserProfile()
profile.age = 25   // Output: About to change user's age from 0 to 25
profile.age = 30   // Output: About to change user's age from 25 to 30

In this case:

The willSet observer is called just before the property’s value changes. The newValue constant holds the property’s future value.

Combining willSet and didSet

You can combine both willSet and didSet in the same property if you want to perform actions before and after the change:

class UserProfile {
    var age: Int = 0 {
        willSet {
            print("About to change age from \(age) to \(newValue)")
        }
        didSet {
            print("User's age changed from \(oldValue) to \(age)")
        }
    }
}

let profile = UserProfile()
profile.age = 25   
// Output: About to change age from 0 to 25
// Output: User's age changed from 0 to 25

In this example, both willSet and didSet observers are triggered when the age property is updated.

Applying Property Observers in Real-world Scenarios

Imagine you are building a temperature sensor for a weather app, where you need to display a warning if the temperature exceeds a certain threshold.

class TemperatureSensor {
    var temperature: Double = 0.0 {
        didSet {
            if temperature > 30.0 {
                print("Warning: High temperature!")
            }
        }
    }
}

let sensor = TemperatureSensor()
sensor.temperature = 25.0   // No output
sensor.temperature = 32.0   // Output: Warning: High temperature!

In this example, we monitor the temperature property and print a warning if the value exceeds 30 degrees.

Property Observers and Computed Properties

You cannot directly attach property observers (willSet and didSet) to computed properties. However, you can achieve similar behavior by adding the logic directly inside the getter or setter of a computed property.

Here’s an example using a computed property:

class Rectangle {
    var width: Double = 0
    var height: Double = 0
    
    var area: Double {
        get {
            return width * height
        }
        set {
            width = newValue / height
            print("Area updated, new width is \(width)")
        }
    }
}

let rectangle = Rectangle()
rectangle.height = 10
rectangle.area = 50   // Output: Area updated, new width is 5.0

In this example, the area property is a computed property, and its setter contains logic to update the width when the area is set.

Limitations of willSet and didSet in Swift

While willSet and didSet are powerful features in Swift, there are a few limitations and nuances you should be aware of when using them. These limitations can affect how and when you can apply property observers effectively in your projects.

1. Not Available for let Constants

You can only use property observers with var (mutable) properties. If a property is declared as a constant using let, you cannot attach willSet or didSet observers to it because constants cannot be modified after their initial value is set.

Example:

let someValue = 10
// You cannot add willSet or didSet to a constant property like this
2. No Support for Computed Properties

You cannot directly use willSet and didSet on computed properties. Computed properties already have a getter and setter, and their value is determined dynamically. Instead, if you want to observe changes in a computed property, you need to add logic inside the computed property’s setter.

Example (not allowed):

var value: Int {
    willSet {
        // This is not allowed for computed properties
    }
    didSet {
        // This is also not allowed for computed properties
    }
}

Instead, you would handle changes in the computed property’s setter like this:

var value: Int {
    get {
        return someStoredValue
    }
    set {
        someStoredValue = newValue
        // Handle change here
    }
}
3. Cannot Use with Lazy Properties

Lazy properties are initialized only when they are accessed for the first time. Since property observers depend on knowing when the property value changes, you cannot use willSet and didSet with lazy properties. The reasoning is that a lazy property might not even be initialized by the time you want to observe its changes.

Example (not allowed):

lazy var someProperty: Int = 10 {
    willSet {
        print("This won't work")
    }
}
4. Initial Value Setting Doesn’t Trigger Observers

Property observers are not triggered when the initial value of a property is set during initialization. They only get triggered when the property value is explicitly changed after the object has been initialized.

Example:

class UserProfile {
    var age: Int = 20 {
        didSet {
            print("Age changed to \(age)")
        }
    }
}

let profile = UserProfile()
// No output since didSet is not called on initialization
profile.age = 25  // Output: Age changed to 25

In this example, the didSet observer is not called when age is initialized with 20, only when it is changed to 25 later.

5. Risk of Infinite Loops

If you modify a property inside its own didSet or willSet observer, you may accidentally trigger an infinite loop. This happens when the observer modifies the property, which triggers the observer again, and so on.

Example:

var count: Int = 0 {
    didSet {
        count += 1  // This will trigger didSet again, causing an infinite loop
    }
}

In this case, the didSet observer modifies count, which causes the observer to run again, leading to an infinite loop and eventually a crash.

6. No Observers for Global and Local Variables

Property observers work only for class, struct, or enum instance properties. You cannot attach willSet or didSet to global or local variables outside the scope of these types.

Example (not allowed):

var someGlobalValue: Int = 10 {
    willSet {
        // Will not work on global variables
    }
    didSet {
        // Will not work on global variables
    }
}
7. Cannot Use with Property Wrappers

If you’re using Swift’s property wrappers (@propertyWrapper), you cannot directly combine them with willSet and didSet observers. This is because property wrappers themselves manage the storage and behavior of the property, overriding the need for traditional observers. However, you can often achieve the desired behavior inside the wrapper.

Example (not allowed):

@propertyWrapper
struct Lowercase {
    var wrappedValue: String {
        didSet {
            wrappedValue = wrappedValue.lowercased() // This does not work directly with observers
        }
    }
}

You would need to implement such behavior within the property wrapper’s logic.

Summary of Limitations

Here’s a quick overview of the limitations:

  • No support for let constants: willSet and didSet only work with var properties.
  • No direct use with computed properties: Use logic inside the getter and setter instead.
  • Cannot use with lazy properties: Lazy properties defer initialization, making property observers incompatible.
  • No observers on initialization: Property observers are not triggered when the property is first initialized.
  • Risk of infinite loops: Be cautious when modifying a property inside its own observer.
  • No observers for global or local variables: They only work for properties within classes, structs, or enums.
  • Cannot combine with property wrappers: Direct use of willSet and didSet with property wrappers is not allowed.
  • Despite these limitations, property observers remain a powerful feature for managing state changes in Swift, especially when used appropriately within the constraints mentioned.

Best Practices for Using Property Observers

  • Avoid Complex Logic in Observers: Property observers should be lightweight. If you need to perform complex operations, consider moving that logic to a separate function.
  • Use willSet Sparingly: In many cases, you’ll only need the didSet observer, which is more useful for validating or reacting to a change that has already occurred.
  • Avoid Infinite Loops: Be cautious of updating a property inside its own didSet or willSet observer. Doing so can cause an infinite loop if you’re not careful.

Conclusion

Property observers (willSet and didSet) are powerful tools in Swift that allow you to monitor and respond to changes in property values efficiently. They make it easy to track data changes, enforce rules, or update other parts of your application, such as the UI. Whether you’re developing simple applications or complex systems, mastering property observers can help you write cleaner, more reactive Swift code.

Understanding when and how to use property observers will enable you to handle state changes more effectively and build more dynamic, responsive applications.

Happy coding!

Comments