DEV Community

Gamya
Gamya

Posted on

Swift Structs โ€” Property Observers (willSet & didSet) ๐Ÿ‘€

So far we've seen how to store values in properties and calculate them dynamically with computed properties. But what if you want to react every time a property changes โ€” automatically, without having to remember to call a function yourself? That's exactly what property observers are for. ๐Ÿฅ

Swift gives us two flavors:

  • didSet โ€” runs after the property changes
  • willSet โ€” runs before the property changes

The Problem Without Property Observers

Imagine we're tracking a ninja's battle score:

struct BattleTracker {
    var score = 0
}

var naruto = BattleTracker()
naruto.score += 10
print("Score is now \(naruto.score)")
naruto.score -= 3
print("Score is now \(naruto.score)")
naruto.score += 1
// Oops โ€” forgot to print this one! ๐Ÿ›
Enter fullscreen mode Exit fullscreen mode

We have to remember to print the score every time it changes. Forget once โ€” like that last line โ€” and you've got a bug. In a small example that's easy to spot, but in a real app with dozens of places updating a property? That's a disaster waiting to happen.


didSet โ€” React After the Change

With didSet, we attach the print directly to the property. Now it runs every time the score changes, no matter where the change happens:

struct BattleTracker {
    var score = 0 {
        didSet {
            print("Score updated to \(score)!")
        }
    }
}

var naruto = BattleTracker()
naruto.score += 10  // "Score updated to 10!"
naruto.score -= 3   // "Score updated to 7!"
naruto.score += 1   // "Score updated to 8!"
Enter fullscreen mode Exit fullscreen mode

No more forgotten print statements. Swift handles it automatically. ๐ŸŽ‰

Accessing the Old Value

Inside didSet, Swift automatically gives you oldValue โ€” the value the property had before the change. This is useful when you want to show what changed:

struct Ninja {
    var chakraLevel = 1000 {
        didSet {
            print("Chakra changed from \(oldValue) to \(chakraLevel)")
        }
    }
}

var sasuke = Ninja()
sasuke.chakraLevel -= 300 // "Chakra changed from 1000 to 700"
sasuke.chakraLevel -= 150 // "Chakra changed from 700 to 550"
Enter fullscreen mode Exit fullscreen mode

oldValue is automatically provided โ€” you don't need to declare it yourself.


willSet โ€” React Before the Change

willSet works the same way but fires before the property changes. Inside it, Swift automatically provides newValue โ€” the value that's about to be assigned:

struct Guild {
    var members = [String]() {
        willSet {
            print("Current members: \(members)")
            print("About to change to: \(newValue)")
        }

        didSet {
            print("Guild now has \(members.count) members.")
            print("Previous list was: \(oldValue)")
        }
    }
}

var akatsuki = Guild()
akatsuki.members.append("Pain")
akatsuki.members.append("Konan")
Enter fullscreen mode Exit fullscreen mode

Both willSet and didSet fire when you append to an array โ€” because appending is a change to the property.


willSet vs didSet โ€” Which Should You Use?

In practice, didSet is what you'll use most of the time. The reason is simple: you usually want to react after something has changed โ€” update the UI, save data, log something.

willSet is less common, but it shines when you need to capture the state before the change happens. SwiftUI itself uses willSet in certain places for animations โ€” it takes a snapshot of the UI before the change, then compares it with the "after" snapshot to figure out what needs to animate.

A quick summary:

Observer Fires Automatic value available
willSet Before the change newValue (the value coming in)
didSet After the change oldValue (the value that just left)

Why Use Observers Instead of Just Calling a Function?

You might be thinking: "couldn't I just call a function manually every time I update a property?" Yes โ€” but then you have to remember to do it. Every. Single. Time.

// Without observers โ€” easy to forget:
naruto.chakraLevel -= 300
logChakraChange() // what if you forget this line?

naruto.chakraLevel -= 150
// Forgot to call logChakraChange() here โ€” silent bug ๐Ÿ›
Enter fullscreen mode Exit fullscreen mode

With a didSet observer, you only write the logic once, attached to the property itself. Swift guarantees it runs every time the property changes, no matter where in your code that happens. You're transferring the responsibility to Swift so your brain can focus on more interesting things. ๐ŸŒธ


One Important Warning

Keep property observers lightweight. If score += 1 looks like a simple operation, the reader of your code (including future you) will expect it to be fast. If your didSet runs an expensive network call or sorts a massive array every time a number increments, you'll get mysterious slowdowns that are hard to trace.

A good rule of thumb: property observers should do quick reactive work โ€” logging, updating a UI label, playing a sound effect. Heavy work belongs in a function you call explicitly, where the cost is obvious. ๐ŸŒธ


Wrap Up

Concept What It Means
didSet Runs after a property changes; oldValue is available
willSet Runs before a property changes; newValue is available
oldValue The value the property had before the change (inside didSet)
newValue The value about to be assigned (inside willSet)
Why observers? Swift guarantees they run โ€” no forgetting to call a function

This article was written by me; AI was used to improve grammar and readability.


Top comments (0)