Stop firing didSet to avoid infinite recursion with didSet in Swift with multiple properties

128 Views Asked by At

I have a class with two properties whose value is set using didSet.

class MyClass {
    var myProp1: Bool {
        didSet {
            self.myProp2 += blinks ? 1 : -1
        }
    }
    var myProp2: Int {
        didSet {
            self.myProp1 = (midi % 2) != 0
        }
    }
}

This will result – as expected – in infinite recursion (myProp1 calling myProp2 calling myProp1...).

Is it possible to "turn off" the didSet behaviour when a property is called in another didSet? I know that didSet doesn't fire in init() and this should be similar.

I know about get/set and I know about the way to create a method, but I'm asking about suppressing didSet behaviour causing infinite recursion.

2

There are 2 best solutions below

6
HangarRash On BEST ANSWER

There's no way to disable a call to didSet. In a case where you have two properties where setting one should update the other, you need a temporary property to be able to tell the other didSet not to do anything when being called indirectly through the first didSet.

Here's one working solution using a private Bool and some extra checks (I renamed the properties to match what you had in the didSet blocks to make a working example):

class MyClass {
    var blinks: Bool {
        didSet {
            if !avoidLoop {
                avoidLoop = true
                self.midi += blinks ? 1 : -1
                avoidLoop = false
            }
        }
    }
    var midi: Int {
        didSet {
            if !avoidLoop {
                avoidLoop = true
                self.blinks = (midi % 2) != 0
                avoidLoop = false
            }
        }
    }

    // Added so the code can be tested
    init() {
        blinks = false
        midi = 0
    }

    private var avoidLoop = false
}

Here's some sample code that runs without any problems with infinite recursion:

var xxx = MyClass()
print(xxx.blinks, xxx.midi)
xxx.blinks = true
print(xxx.blinks, xxx.midi)
xxx.midi = 6
print(xxx.blinks, xxx.midi)

Output:

false 0
true 1
false 6

0
Alexander On

The given example problem is too abstract and mysterious to give a good alternative solution. Perhaps the best solution to your real underlying problem doesn't even involve property observers, but there's no way to know without more detail.

One way to prevent this recursion is to split your public APIs from some backing properties. The public computed properties here only update the private stored properties, but never each other, so there's no recursion.

class MyClass {
    let blinks = false
    let midi = 1
    
    public var myProp1: Bool {
        get { _myProp1 }
        set {
            self._myProp1 = myProp1
            self._myProp2 += blinks ? 1 : -1 // no recursion here
        }
    }
    public var myProp2: Int {
        get { _myProp2 }
        set {
            self._myProp1 = !midi.isMultiple(of: 2) // no recursion here
            self._myProp2 = myProp2
        }
    }
    
    private var _myProp1: Bool
    private var _myProp2: Int
}

I strongly suspect that the best approach here would involve splitting myProp1 and myProp2 into a new struct, and updating it as a whole on every mutation.