4

It's well-known that, of course, didSet will not run on the same object again from inside a didSet. (example.)

However. It seems that: the restriction applies not only to that object, but to maybe any object of the same class.

Here are copy-paste test cases for Playground.

class C {
    var Test: Bool = false {
        didSet {
            print("test.")
            for c in r {
                c.Test = true
            }
        }
    }
    var r:[C] = []
}
var a:C = C()
var b:C = C()
var c:C = C()
a.r = [b, c]
a.Test = false

Does not work!

class C {
    var Test2: Bool = false {
        didSet {
            print("test2.")
            global.Test2 = true
        }
    }
}
var global:C = C()
var a:C = C()
a.Test2 = false

Does not work!

  1. Is this a Swift bug?

  2. If not, what is the actual restriction? It won't run ANY didSet (whatsoever) that starts from a didSet?; the same identical class?; the same super class?; or?

  3. Where exactly is this explained in the doco?

WTF. One needs to know ... what is the actual restriction specifically?

Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • Theoretically one of the children could be itself. meaning an infinite loop could still occur. Might be a bug, might be a feature It's not an answer, hence the reason it's in the comments :) – GetSwifty Feb 09 '17 at 20:26
  • 1
    @JoeBlow: This comment in the [Swift source code](https://github.com/apple/swift/blob/master/lib/AST/Decl.cpp#L1072) might be relevant: *"Observing member are accessed directly from within their didSet/willSet specifiers. This prevents assignments from becoming infinite loops."* You could also try to ask at the swift-users mailing list, where people from the Swift team are regularly contributing. – Martin R Feb 09 '17 at 22:05
  • This looks like the same question: [Why no Infinite loop in didSet?](http://stackoverflow.com/questions/29363170/why-no-infinite-loop-in-didset). – Martin R Feb 09 '17 at 22:09
  • 1
    I completely agree that the Swift implementation is surprising and I would prefer an infinite loop there. – Sulthan Feb 10 '17 at 11:10

1 Answers1

1

This is bug SR-419.

From the comment on the bug:

Ugh. We really need to check that the base of the property access is statically self.

and from my experiments it seems that the didSet observer is not invoked only if you set the same property on any object. If you set any other property (even on the same object), the observer is invoked correctly.

class A {
    var name: String
    var related: A?
    var property1: Int = 0 {
        didSet {
            print("\(name), setting property 1: \(property1)")

            self.property2 = 100 * property1
            related?.property1 = 10 * property1
            related?.property2 = 100 * property1
        }
    }
    var property2: Int = 0 {
        didSet {
            print("\(name), setting property 2: \(property2)")
        }
    }

    init(name: String) {
        self.name = name
    }
}

let a = A(name: "Base")
a.related = A(name: "Related")
a.property1 = 2

Output:

Base, setting property 1: 2
Base, setting property 2: 200
Related, setting property 2: 200

when the expected output should be:

Base, setting property 1: 2
Base, setting property 2: 200
Related, setting property 1: 20
Related, setting property 2: 2000
Related, setting property 2: 200

It seems you also need to assign that property directly from the observer. Once you enter another function (or observer), the observers start working again:

var property1: Int = 0 {
    didSet {
        print("\(name), setting property 1: \(property1)")

        onSet()
    }
}

...    
func onSet() {
    self.property2 = 100 * property1
    related?.property1 = 10 * property1
    related?.property2 = 100 * property1
}

And that is the best workaround.

Another workaround (thanks @Hamish) is to wrap nested assignments into an immediately executed closure:

var property1: Int = 0 {
    didSet {
       {
           self.property2 = 100 * property1
           related?.property1 = 10 * property1
           related?.property2 = 100 * property1
       }()
    }
}

Depending on code before the closure, you might have to wrap it into parenthesis or insert a semicolon after the preceding statement.

Community
  • 1
  • 1
Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • @JoeBlow From my testing it works correctly for unrelated properties. Also, if you call `property1.didSet` from `property2.didSet` which is called from `property1.didSet` then you get a nice infinite loop like expected. I haven't tested subclasses but I think it would work the same - it's still the same property. – Sulthan Feb 12 '17 at 14:25
  • 1
    Another interesting workaround – using an immediately-executed closure: `_ = { self.related?.property1 = 10 * self.property1 }()` (for some reason, Swift doesn't like it if you don't say `_ = `). – Hamish Feb 12 '17 at 18:29
  • @Hamish you would have to add a manual return to make it return void :) – Sulthan Feb 12 '17 at 20:34
  • @Sulthan Not sure I follow – an assignment returns `Void` (although in this case it's an optional chain assignment, so `Void?`). Thus the closure should be inferred to be a `() -> Void?` and all should be well. Swift however complains that it's expecting a `do {...}` block, but adding a `_ =` makes the error go away. Adding a `; return` in the closure doesn't help. – Hamish Feb 12 '17 at 20:41
  • @Hamish Sorry, I was reading wrong from mobile. You would have to wrap the block into parenthesis `({ ... })()` otherwise the syntax analyzer will have a hard time figuring it's a closure. – Sulthan Feb 13 '17 at 09:31
  • @Sulthan Yeah, I'm aware there's various corner cases with the syntax parser, my point was that `{ self.related?.property1 = 10 * self.property1 }()` should be valid – although adding parenthesis is a good suggestion to work around it (a workaround to implement a workaround :) ) – Hamish Feb 13 '17 at 10:19
  • @Hamish You can also make `{ ... }` to be a closure by adding `{ () -> Void in ... }`, however you can have other collisions because the compiler can consider it a trailing closure for a previous expression. – Sulthan Feb 13 '17 at 10:26
  • 1
    @Sulthan Ah, that seems to be it – if there's no previous statement, then it compiles fine (you don't need to add `() -> Void in`). You can also fix it by adding a semicolon to the previous statement, forcing the parser to treat them separately. – Hamish Feb 13 '17 at 10:29
  • @Hamish Ah, didn't think of semicolon. – Sulthan Feb 13 '17 at 10:30
  • @JoeBlow True but it's not that common to have classes that hold classes of the same type. – Sulthan Feb 13 '17 at 12:36
  • @JoeBlow I did consider writing an answer, but decided against it due to the fact that Sulthan and Martin have already covered the why and what of the matter – and that's what I thought your question was asking for (to quote you: "*PS, obviously, I know how to write a workaround here*"). I was really just hoping Sulthan would add the workaround as an addendum to his answer :) – Hamish Feb 13 '17 at 17:15
  • @Hamish I have added that now. – Sulthan Feb 13 '17 at 17:58
  • FYI this will be fixed in Swift 5 :) – Hamish Jan 11 '19 at 23:05