Search code examples
swift3property-observer

Swift's property oveservers don't get called in cascade


Let's say I have a simple tree-like structure like this:

class Tree {
    weak var ancestor: Tree?
    var children = [Tree]()
}

Every node of the tree keeps a reference to its children, and a weak one (to avoid reference cycles) to the oldest ancestor.

I'd like to automatically update the ancestor property of the all nodes of a subtree whenever I set it on its root.

let grandParent = Tree()
let parent      = Tree()
let child       = Tree()
let grandChild  = Tree()

child.children.append(grandChild)
parent.children.append(child)
grandParent.children.append(parent)

// Should set ancestor in the whole hierarchy.
grandParent.ancestor = grandParent

I've tried to achieve that with property observers, iterating on the node's children whenever its ancestor is set so that the value is propagated, but noticed neither didSet nor willSet gets called passed the first child. Here's the code I've been experiencing with:

weak var ancestor: Tree? = nil {
    didSet {
        for child in self.children {
            child.ancestor = self.ancestor
        }
    }
}

As expected, the ancestor of grandParent is properly set, and so is that of the parent. However, those of its descendants (child and grandChild) are not. Note that I've been observing the same result with willSet (of course adjusting the above code to take into account that self.ancestor wouldn't have changed yet).

Could someone point to me what I'm missing here?


I could do what I want with computed properties, with a very similar approach. But I find it less elegant than the one with property observers and would rather avoid it if possible. Nevertheless, this snippet gets things done.

var ancestor: Tree? {
    get {
        return self._ancestor
    }

    set(newAncestor) {
        self._ancestor = newAncestor
        for child in self.children {
            child.ancestor = newAncestor
        }
    }
}

private weak var _ancestor: Tree? = nil

Solution

  • I've seen this mentioned before, and it seems to be a Swift bug that is triggered if you try to set a property inside a willSet / didSet on another instance of the same type.

    Oddly enough, syntax matters and seems to work around the bug. For example, I can make your code work by changing your didSet to this:

    weak var ancestor: Tree? = nil {
        didSet {
            children.forEach { $0.ancestor = ancestor }
        }
    }
    

    It's doing essentially the same thing, but doesn't trigger the compiler bug.


    UPDATE

    This has previously been filed as a bug on swift.org and is still open: https://bugs.swift.org/browse/SR-419

    And there are a couple additional comments clarifying the cause and what workarounds can avoid it here: https://twitter.com/uint_min/status/804795245728698368