'withObservationTracking' firing onChange even if value doesn't change

I did a small experiment with the following code:

@Observable
class State {
    var value = 0
    var isSomething = false
}

and

var state = State()

func printOnChange() {
    withObservationTracking {
        print("Value is now: \(state.isSomething)")
    } onChange: {
        printOnChange()
    }
}

printOnChange()

state.isSomething = true
state.isSomething = true
state.isSomething = false
state.isSomething = false

And the output I'm getting is

Value is now: false
Value is now: false
Value is now: true
Value is now: true
Value is now: false

I thought that the on onChange was supposed to be fired only twice.
Am I getting this wrong?

I'm using the windows toolchain, by the way.

Observation doesn't have a concept of equality (i.e. it doesn't require observed types to be Equatable and doesn't do any == checks); instead, it just fires on every write. You can filter out the redundant notifications yourself if you wish (although this might not be what you want: see below).

Also, it prints five times (instead of four, as there are four writes per above) because you're printing in the apply closure, which purpose is to read out (and register for) the value. The reason it prints false two times initially is that it's observing the willSet event (which is undocumented and IMO unintuitive for being the default), but this means that it will read out the value prior the write until the onChange closure returns. Here's the place.

The intended way to use (this flavour of) observation is to perform "invalidation" of sorts by setting dirty flags, scheduling render updates etc. The onChange closure is there to signal about an occurring write in a lightweight form, not for performing any major logic or data diffing within it.

Looks like “onChange” is an improper name then.

Thanks @nkbelov. So it behaves more or less like InvalidationListeners in other languages.