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...
MijickPopups Hero

  Popups Alerts Resizable Sheets Banners

SwiftUI Tooltip

This package provides you with an easy way to show tooltips over any SwiftUI view, since Apple does not provide ...

SimpleToast for SwiftUI

SimpleToast is a simple, lightweight, flexible and easy to use library to show toasts / popup notifications inside iOS or ...

SSToastMessage

Create Toast Views with Minimal Effort in SwiftUI Using SSToastMessage. SSToastMessage enables you to effortlessly add toast notifications, alerts, and ...

ToastUI

A simple way to show toast in SwiftUI   Getting Started • Documentation • Change Log