clayellis
(Clay Ellis)
1
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
Lantua
2
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. 
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.
clayellis
(Clay Ellis)
4
That would make sense in the context of an ObservableObject.
clayellis
(Clay Ellis)
5
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
}
Lantua
8
Hmm, it's weird that it's not documented.
1 Like
Lantua
10
It does seem to lean toward post-modification, but could be the way it's phrased.
clayellis
(Clay Ellis)
11
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?
BigZaphod
(Sean Heber)
13
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
}
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.
clayellis
(Clay Ellis)
17
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
clayellis
(Clay Ellis)
18
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.
clayellis
(Clay Ellis)
20
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?