How to take action when a property changes
  • July 29, 2025

1. Taking Action When a Property Changes: Property Observers

Swift lets you observe and respond to changes in a property’s value by attaching willSet and didSet observers to any stored property (except lazy and let constants).

class TemperatureSensor {
    var temperature: Double = 0.0 {
        willSet {
            print("About to change from \(temperature)° to \(newValue)°")
        }
        didSet {
            let delta = temperature - oldValue
            print("Changed by \(delta)°")
            if temperature > 100 {
                print("Warning: Overheat!")
            }
        }
    }
}

let sensor = TemperatureSensor()
sensor.temperature = 75.0
// About to change from 0.0° to 75.0°
// Changed by 75.0°

sensor.temperature = 120.0
// About to change from 75.0° to 120.0°
// Changed by 45.0°
// Warning: Overheat!

Optional: When to Use Property Observers

  • Keep related state in sync
    When setting one property requires updating UI, logging, or recomputing dependent values.
  • Side-effects
    Trigger animations, database writes, notifications, network calls, etc., in response to changes.

Optional: Choosing willSet vs. didSet

  • willSet
    Use when you need to know the incoming value before it’s assigned (e.g. validate or veto, update UI placeholder).
  • didSet
    Use when you need to react after the new value is in place (e.g. recalculate, notify observers, animate from old to new).

Test: Property Observers

class Counter {
    var count: Int = 0 {
        willSet {
            print("Counter will change from \(count) to \(newValue)")
        }
        didSet {
            print("Counter did change from \(oldValue) to \(count)")
        }
    }
}

// Test:
let counter = Counter()
counter.count = 1
// Expect:
// Counter will change from 0 to 1
// Counter did change from 0 to 1

counter.count = 5
// Expect:
// Counter will change from 1 to 5
// Counter did change from 1 to 5

2. Creating Custom Initializers

Swift structs and classes get a default memberwise initializer (for structs) or a simple default initializer (for classes without stored-property defaults), but you can—and often should—define your own initializers to enforce invariants or provide convenient defaults.

struct User {
    let username: String
    var age: Int
    var isPremium: Bool

    // Custom initializer
    init(username: String, age: Int = 18, isPremium: Bool = false) {
        self.username = username
        // can validate or adjust values if needed
        self.age = max(age, 0)         // no negative ages
        self.isPremium = isPremium
    }
}

// Usage:
let guest = User(username: "GuestUser")
// guest.age == 18, guest.isPremium == false

let vip = User(username: "Star", age: 30, isPremium: true)

Optional: How Memberwise Initializers Work

  • Structs automatically get an initializer that lists all stored properties (unless you define any custom initializer, which suppresses the memberwise one).
  • Classes do not get memberwise initializers; they get a default init() only if all stored properties have default values.

Optional: When to Use self in a Method or Initializer

  • Disambiguation
    When a parameter or local variable has the same name as a property:

    init(name: String) {
      self.name = name
    }
    
  • Clarify intent
    self makes it explicit you’re referring to the instance, especially inside closures or nested scopes.

Test: Initializers

struct Rectangle {
    var width: Double
    var height: Double

    // Custom initializer enforcing positive dimensions
    init(width: Double, height: Double) {
        self.width  = max(width, 0)
        self.height = max(height, 0)
    }
}

// Test:
let r1 = Rectangle(width: 5, height: 3)
assert(r1.width == 5 && r1.height == 3)

let r2 = Rectangle(width: -2, height: 4)
assert(r2.width == 0 && r2.height == 4)
print("Initializer tests passed")

Test: Referring to the Current Instance

class Circle {
    var radius: Double

    init(radius: Double) {
        self.radius = radius         // use of self to disambiguate
    }

    func scale(by factor: Double) {
        self.radius *= factor        // self makes it clear we're modifying the property
    }

    func description() -> String {
        // self is optional here, but allowed
        return "Circle with radius \(self.radius)"
    }
}

// Test:
let circle = Circle(radius: 2)
circle.scale(by: 3)
assert(circle.radius == 6)
print(circle.description())  // "Circle with radius 6.0"

 

YOU MIGHT ALSO LIKE...
PermissionsSwiftUI: A SwiftUI package to handle permissions

PermissionsSwiftUI displays and handles permissions in SwiftUI. It is largely inspired by SPPermissions. The UI is highly customizable and resembles an Apple style. ...

Pager tab strip view

Introduction PagerTabStripView is the first pager view built in pure SwiftUI. It provides a component to create interactive pager views ...

PageView

SwiftUI view enabling page-based navigation, imitating the behaviour of UIPageViewController in iOS.

Pages

    

How to create your own structs? How to compute property values dynamically?

1. Creating Your Own Structs In Swift, a struct is a value type that you define with the struct keyword. ...