Mutating Struct and State Observers: How the Latter Will Be Notified Even for 'No-Ops'

I may be 7 years late or so – but until last week, I didn’t realize that didSet property observers would fire when the observed property hasn’t actually changed. All you need is a mutating func that doesn’t even need to mutate.

Observation

This can be illustrated with a simple piece of code:

struct State {
    struct InnerState {
        mutating func touch() {
            print("InnerState.touch")
        }
    }

    var innerState: InnerState {
        didSet {
            print("did set to \(innerState)")
        }
    }
}

var state = State(innerState: .init())
state.innerState.touch()
// => InnerState.touch
// => did set to InnerState(value: 1)

Please don’t throw stones, for I was suffering from magical thinking:

I was under the (wrong) assumption that the mutating func needed to, well, somehow mutate the receiver of that method call, like change a property value. And that this in turn would be noted “somewhere”. Conversely, I was under the (wrong) assumption that a mutating func without any mutations inside would behave 100% like a regular, non-mutating function.

To my surprise, the innerState.touch() call marks innerState as mutated, and thus the accompanying State’s property observer fires. So now I have to rethink quite some stuff.

Consequences

Here’s two consequences that come to mind for my day-to-day work:

Expose changes to reference type objects in your value types

A pretty nice consequence is that you can use mutating func to change a reference type property inside a value type, and have references to the value type still know that it has changed:

class ReferenceType { ... }

struct ValueType {
    // `private` to discourage direct access, bypassing the mutating func:
    private let referenceType = ReferenceType()
    mutating func touchReferenceType() {
        referenceType.doSomething()
    }
}


var valueType: ValueType = ... {
    didSet { print("did change") }
}

valueType.touchReferenceType()
// => did change

This technique requires that you don’t reach inside the ValueType and perform mutations inside its reference type property directly; everything needs to go through a mutating func as a Façade or Adapter of sorts.

Value type app state changes could fire without actually changing anything

If you use ReSwift or another approach to unidirectional flow based on state representations via value types, calling a mutating func will notify all state subscribers of the change – even when there hasn’t been a change.

As a consequence, the following code with a guard statement that exits early (and does not mutate) will still trigger a didSet property observer of State:

struct State {
    // ...
    mutating func handle(action: Action) {
        guard action.satisfiesSomeCondition else { return }
        self.substate = action.newValue
    }
}

This handle(action:) function is a generic placeholder for state reducers.

Usually, you’ll be checking for oldState != newState in your subscriber to filter idempotent states. But this will trigger the state equality check even when nothing has changed.

That got me thinking: should I be filtering potentially expensive state checks before they reach the reducers?

In ReSwift, there are middlewares that can “swallow” or filter actions, i.e. refuse to forward the incoming action to the state. So:

When comparing e.g. large strings or complex objects for equality, and if these checks become a performance bottleneck or happen at high frequencies, it may be useful to filter the actions before they trigger the didSet property observer for the state.