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

How to use trailing closures and shorthand syntax?

1. Trailing Closure Syntax When the last parameter to a function is a closure, you can write that closure after ...

How to create and use closures?

1. What Is a Closure (and Why Swift Loves Them) A closure in Swift is a self-contained block of functionality ...

How to provide default values for parameters How to handle errors in functions

1. Providing Default Values for Function Parameters (Deep Dive) 1.1 Syntax and Ordering Declaration You assign a default right in ...

exyte

Concentric Onboarding iOS library for a walkthrough or onboarding flow with tap actions written with SwiftUI         Usage Create View's ...