How to Use willSet and didSet Property Observers in Swift

How to Use willSet and didSet Property Observers in Swift

Written by

in

Quick answer: In Swift, willSet and didSet are property observers attached directly to stored properties to monitor value changes without requiring a separate backing variable. Use willSet to inspect the upcoming newValue before assignment, and didSet to inspect oldValue or trigger side effects immediately after assignment.

Why this happens

Developers coming from languages like C# or Objective-C often confuse Swift’s get and set blocks with its willSet and didSet observers. In Swift, providing a custom set block turns a declaration into a computed property. Computed properties do not allocate storage for values; they rely on computation or an explicit secondary backing property.

Attempting to assign a value to a property directly inside its own set block results in infinite recursion because assigning to the property invokes the setter again, leading to compiler warnings or runtime stack-overflow crashes:

// Error: Recursive setter call
var score: Int {
    get {
        return score
    }
    set {
        score = newValue // Calls 'set' again infinitely
    }
}

To run side effects—such as reloading a view, updating a dependent value, or validating input—without maintaining an extra private backing field (like _score), Swift provides property observers via willSet and didSet on stored properties.


How to fix it

1. Attach didSet to a Stored Property for Post-Assignment Side Effects

The most common use case for property observers is executing code after a property’s value has already changed. Inside didSet, you can access the previous value through the automatic constant oldValue.

class ProfileViewModel {
    var username: String = "" {
        didSet {
            print("Username updated from '\(oldValue)' to '\(username)'")
            validateUsername()
        }
    }

    private func validateUsername() {
        // Perform validation or trigger UI updates
    }
}

let profile = ProfileViewModel()
profile.username = "alex_dev"
// Prints: Username updated from '' to 'alex_dev'

Unlike a custom setter, the stored value is automatically saved in username. You do not have to write boilerplate storage code.

2. Use willSet When You Need to Inspect the Incoming Value

The willSet observer runs immediately before the property stores the new value. Swift supplies an automatic constant named newValue inside this block. Use willSet when an animation, logging mechanism, or transition coordinator needs to know what is about to change before storage updates.

class AudioPlayer {
    var volume: Float = 1.0 {
        willSet {
            print("Adjusting volume from \(volume) to \(newValue)")
        }
        didSet {
            if volume < 0.0 {
                volume = 0.0
            } else if volume > 1.0 {
                volume = 1.0
            }
        }
    }
}

let player = AudioPlayer()
player.volume = 1.2
// Prints: Adjusting volume from 1.0 to 1.2
// Clamped to 1.0 inside didSet

Note: Assigning to a property inside its own didSet updates the property to the final clamped value without triggering didSet a second time.

3. Use Custom Names for Parameters

While newValue and oldValue are provided automatically by Swift, you can provide explicit names in parentheses following willSet or didSet if it clarifies complex domain logic.

struct TemperatureTracker {
    var currentCelsius: Double = 0.0 {
        willSet(upcomingTemp) {
            if upcomingTemp > 100.0 {
                print("Warning: Incoming reading of \(upcomingTemp)°C exceeds boiling point.")
            }
        }
        didSet(previousTemp) {
            let delta = currentCelsius - previousTemp
            print("Temperature shifted by \(delta)°C")
        }
    }
}

4. Choose Computed Properties When True Calculation or Backing Storage is Required

If you genuinely need full control over read and write operations—such as converting units or deriving a value from other states—use a computed property with get and set. Remember to route the data through a separate stored variable to avoid recursion.

struct DistanceConverter {
    private var internalMeters: Double = 0.0

    var meters: Double {
        get {
            return internalMeters
        }
        set {
            internalMeters = max(0, newValue)
        }
    }

    var kilometers: Double {
        get {
            return internalMeters / 1000.0
        }
        set {
            internalMeters = newValue * 1000.0
        }
    }
}

How to prevent it

  • Do not write getters alongside observers: In Swift, a property cannot define both get/set and willSet/didSet simultaneously on the same declaration.
  • Remember initialization rules: Neither willSet nor didSet is executed during an initializer (init) when assigning initial values to stored properties.
  • Watch out for lazy properties: Swift does not allow property observers on properties marked with the lazy keyword.
  • Beware of mutating structs: If you pass a struct with property observers into an inout function parameter, didSet executes when the function exits even if the final value matches the original value.

FAQ

Do willSet and didSet trigger inside init?

No. When an instance is initialized, assigning default values directly or within an init method does not invoke willSet or didSet. Property observers only begin monitoring changes after initialization completes.

Can I call didSet on property override in a subclass?

Yes. A subclass can add willSet or didSet observers to any inherited stored or computed property by overriding the property using the override keyword.

Does changing a value inside its own didSet trigger an infinite loop?

No. If you assign a new value to a property from within its own didSet observer, Swift suppresses further observer calls for that specific assignment, preventing infinite recursion.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.