Is this a bug in @Published?

Is this a bug in the @Published property wrapper or am I expecting a different behavior because I've misunderstood something fundamental?

Given:

import Combine

class Test {

    @Published var value = 0

    var valuePublisher: AnyPublisher<Int, Never> {
        $value.eraseToAnyPublisher()
    }
}

let t = Test()

let s = t.valuePublisher.sink { value in
    print("closure value: \(value)")
    print("object value: \(t.value)")
}

t.value = 42

I would expect the following output:

closure value: 0
object value: 0
closure value: 42
object value: 42

But this is the actual output:

closure value: 0
object value: 0
closure value: 42
object value: 0 <-- Expected 42

I would expect that the object's value itself would be updated before publishing the change to its subscribers such that inside of sink the value is the same as the object's value. Is that expectation wrong?

6 Likes

They changed the publisher to objectWillChange (from objectDidChange), so that’d make sense.

Edit:
That'd be only for ObservableObject. I read a little too fast. :stuck_out_tongue_winking_eye:

What happens if you you assign a second time? If it then prints 42, then it‘s a willSet instead of a didSet event. However if it‘s still 0 then I have no idea and it feels like a bug.

That would make sense in the context of an ObservableObject.

The total output is:

closure value: 0
object value: 0

closure value: 42
object value: 0

closure value: 14
object value: 42

So yeah, the value is being published in willSet before it's set. But even then, this feels wrong. @Tony_Parker any input? Is this expected behavior?

1 Like

Looks like a will set to me, but I‘m not sure if that‘s expected behavior here.

Actually that should be unrelated. IIRC Published always emitted the latest value through its publisher.

You can imagine the internals to be more like this:

set {
  // will set
  outerSelf.objectWillChange.send()
  // did set that pushes the value through the publisher
  outerSelf[keyPath: wrapperKeyPath].storage = newValue
}

Hmm, it's weird that it's not documented.

1 Like

The Combine book says:

“Whenever you modify the value of age, $age will emit that new value.”

It does seem to lean toward post-modification, but could be the way it's phrased.

But the question is, where it's emitted, if you access the value itself, will it be the same as the emitted value? (According to the example above, no.) If not, should it be?

That is a fair question, but it would be strange to call this a didSet while the publisher fires before the property is truly set.

Can you emit on main queue and see if that changes anything?

I don't know anything about Combine, but this creates an infinite loop which is... fun..

let s = t.valuePublisher.sink { value in
	print("closure value: \(value)")
	print("object value: \(t.value)")
	t.value = value
}

As it should :sweat_smile:

That is expected, it‘s like if you mutate a property during didSet observer. (Not sure if the compiler allows it though.)

Ah and sorry for confusion, I had closure and object values flipped in my head. However my previous statement about a didSet event remains.

That actually does resolve the issue (I can't control what queue @Published publishes on, but I can control what I receive on):

This change:

var valuePublisher: AnyPublisher<Int, Never> {
    $value.receive(on: DispatchQueue.main).eraseToAnyPublisher()
}

Produces:

closure value: 42
object value: 42

But it drops the initial value which is likely due to the causes outlined in Combine `.receive(on: RunLoop.main)` loses sent value. How can I make it work? - #40 by somu

But now I'm curious because I'm struggling to understand why receiving on the main queue would alter that behavior? I think I need to re-read that thread.

Are you testing it with a beta device like mentioned in the thread? If not then it works because internally it still uses async which causes the publisher to emit after the property is truly set. However this is still not didSet semantic.

:man_facepalming:t2: I'm not

I'll have to install 10.15.2 beta 3 on a volume and test again. (I assume that beta is included in the list of "associated releases"?)

If anyone else is curious, has the beta installed, and wouldn't mind — will you run the original code in the first post and post the output?