Note2 min read

Property observers already give you the old and new value

willSet and didSet take a parameter, and both have a default name — so naming them explicitly is usually redundant. The rules about when they do not fire matter more.

willSet and didSet each receive the value they are missing: willSet gets the incoming one, didSet gets the previous one. Both have default names, so this compiles as written:

var language = "ObjC" {
  willSet {
    print("about to replace \(language) with \(newValue)")
  }
  didSet {
    print("replaced \(oldValue) with \(language)")
  }
}

Each observer reads one value from the parameter and the other from the property. In willSet the property still holds the old value; in didSet it already holds the new one.

The explicit form — willSet(newValue), didSet(oldValue) — names the parameter exactly what it is called by default, so it adds nothing. It is worth writing only when a different name reads better:

didSet(previousUser) {
  guard previousUser.id != user.id else { return }
  reload()
}

When they do not run causes more confusion than what the parameters are called. There are three rules.

They do not run during initialization

Assigning to a property from within init does not call its observers, including through the memberwise initializer. Setup that belongs in didSet has to be called explicitly at the end of init, or it silently does not happen for the first value.

They do not run on assignment from inside didSet

Writing to the property in its own didSet does not re-enter the observer, which is what stops it recursing. So a didSet that clamps or normalises its own value works, and one written expecting to see its own write come back around does not.

They run on every write, not every change

Setting a property to the value it already holds still fires both observers. If the body triggers a reload or a layout pass, an equality check is usually what was meant:

didSet {
  guard oldValue != items else { return }
  tableView.reloadData()
}

The same applies one level down. If the property holds a struct, mutating one of its fields is a write to the whole property, so settings.fontSize = 14 fires didSet on settings — worth knowing before putting expensive work in one.

Topics