@Observable and didSet

Published by malhal on

When you mark a class as @Observable you can no longer use didSet because the macro converts it to a computed property, e.g. a simple:

var test: String = ""

When expanded becomes:

var test: String = ""
{
    @storageRestrictions(initializes: _test)
    init(initialValue) {
        _test = initialValue
    }
    get {
        access(keyPath: \.test)
        return _test
    }
    set {
        withMutation(keyPath: \.test) {
            _test = newValue
        }
    }
    _modify {
        access(keyPath: \.test)
        _$observationRegistrar.willSet(self, keyPath: \.test)
        defer {
            _$observationRegistrar.didSet(self, keyPath: \.test)
        }
        yield &_test
    }
}

As you can see test has become a computed property that is a wrapper around a private _test property, a bit like how Objective C properties worked. As we know computed properties cannot have didSet so if you write one for an observed property then that code is just ignored by the compiler, it should probably throw an error that this code will never be reached but it currently does it. One way I’ve come up with to implement didSet is to move all the properties down a level, that is make your own public computed property test that has the didSet logic; that reads and writes from _test that is tracked and it generates a __test (double underscore) for the storage, e.g.

private var _test: String = "" // this has the observation logic
public var test: String {
    get {
        _test // tracking reaches through test to _test
    }
    set {
        _test = newValue
        // do your didSet logic here
      }
    }
}

I also posted my workaround to Apple Developer Forums here.

Categories: Swift