SWIFT

Swift Property Observers: Syntax, Use Cases, and Best Practices

Property observers in Swift let you monitor and respond to changes in a property’s value. They allow you to run custom logic either before or after a value is modified, making them ideal for tasks like updating the UI, logging changes, validating input, or syncing state between components.

Unlike computed properties, observers apply to stored properties—those that hold actual values—and are triggered every time a value is assigned, even if it hasn’t changed.


Syntax Overview

Swift supports two types of observers:

  • willSet: Called just before a new value is assigned.
  • didSet: Called immediately after the assignment.

Here’s an example using both:

var score: Int = 0 {
    willSet {
        print("Score will change from \(score) to \(newValue)")
    }
    didSet {
        print("Score changed from \(oldValue) to \(score)")
    }
}

Assigning a value triggers both blocks:

score = 10
// Output:
// Score will change from 0 to 10
// Score changed from 0 to 10

You can use just one observer if the other isn’t needed.


Using newValue and oldValue

Inside the willSet block, the incoming value is available through the newValue keyword, which you can rename for clarity:

willSet(newScore) {
    print("Preparing to set new score: \(newScore)")
}

In the didSet block, oldValue gives you access to the previous value. These keywords let you compare old and new values and respond accordingly.


Applying Observers to Stored Properties

You can attach observers to stored properties in both structs and classes:

struct Temperature {
    var celsius: Double {
        didSet {
            print("Celsius updated to \(celsius)")
        }
    }
}

Every time celsius is updated, the didSet block runs. This is useful when state changes must propagate through different parts of your app.


Practical Use Cases

Property observers are commonly used for:

UI Updates

var username: String = "" {
    didSet {
        nameLabel.text = username
    }
}

Logging Changes

var loggedIn: Bool = false {
    didSet {
        print("User login state changed: \(loggedIn)")
    }
}

Input Validation

var quantity: Int = 1 {
    didSet {
        if quantity < 1 {
            quantity = 1
        }
    }
}

This example ensures that quantity can’t be less than 1.

State Synchronization

var userID: Int = 0 {
    didSet {
        fetchUserDetails(for: userID)
    }
}

You can trigger side effects like API calls when a property changes.


Observing Optional Properties

Observers also work with optionals. Even assigning nil or reassigning the same value will trigger them:

var token: String? {
    didSet {
        print("Auth token updated.")
    }
}

Inheritance Considerations

Observers aren’t inherited by subclasses automatically. If a subclass wants to observe a property, it must override it explicitly:

class Base {
    var name: String = "" {
        didSet {
            print("Base updated name")
        }
    }
}

class Child: Base {
    override var name: String {
        didSet {
            print("Child updated name")
        }
    }
}

Without the override, the subclass won’t execute the base class’s observer logic.


Behavior During Initialization

Observers do not get called when a property is initialized within the same scope:

var message: String = "Hello" {
    didSet {
        print("Message changed")
    }
}

init() {
    message = "Welcome" // didSet won’t run here
}

However, if a superclass assigns a value to a subclass property that has observers, those observers will be triggered.


Static Properties and Observers

Swift doesn’t support observers on static (type-level) stored properties:

static var count: Int = 0 // ❌ Not allowed

As a workaround, you can use a computed property with custom getter/setter logic to simulate observation.


Observers vs. Computed Properties

Understanding the distinction is important:

  • Computed properties don’t store a value; they compute it on access.
  • Stored properties with observers hold actual values and execute logic on assignment.

Example:

var age: Int = 0 {
    didSet {
        print("Age updated")
    }
}

var isAdult: Bool {
    return age >= 18
}

Use observers when you need to monitor and react to changes. Use computed properties when the value is derived from other properties.


Property Wrappers and Observers

When using wrappers like @Published or @State in SwiftUI, you can’t attach willSet or didSet directly to the wrapped property. Instead, rely on Combine publishers or .onChange(of:) to react to changes:

.onChange(of: token) { newToken in
    print("Token changed to \(newToken)")
}

Best Practices

Keep these tips in mind:

  1. Avoid infinite loops — Don’t assign to the same property inside didSet.
  2. Keep logic lightweight — Use observers for side effects, not heavy processing.
  3. Don’t expect them to run during init — They only trigger on post-initialization changes.
  4. Use for side effects only — Don’t compute return values or mutate unrelated state arbitrarily.

Summary

Property observers in Swift give you fine-grained control over how your app responds to data changes. Using willSet and didSet, you can update UI, enforce constraints, trigger sync logic, or log events—all with minimal boilerplate.

While they aren’t a one-size-fits-all tool, knowing when and how to use observers can help you write more responsive and maintainable code. In SwiftUI and beyond, they’re a foundational tool in the reactive programming toolbox.

Learn to Code in Swift for Free
Start learning now
button icon
To advance beyond this tutorial and learn Swift by doing, try the interactive experience of Mimo. Whether you're starting from scratch or brushing up your coding skills, Mimo helps you take your coding journey above and beyond.

Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.

You can code, too.

© 2025 Mimo GmbH

Reach your coding goals faster