I'm subscribing the the built-in User Defaults extension, but it seems to be firing multiple times unnecessarily.
This is the code I'm using:
import Combine
import Foundation
import PlaygroundSupport
extension UserDefaults {
@objc var someProperty: Bool {
get { bool(forKey: "someProperty") }
set { set(newValue, forKey: "someProperty") }
}
}
let defaults = UserDefaults.standard
defaults.dictionaryRepresentation().keys
.forEach(defaults.removeObject)
print("Before: \(defaults.someProperty)")
var cancellable = Set<AnyCancellable>()
defaults
.publisher(for: \.someProperty)
.sink { print("Sink: \($0)") }
.store(in: &cancellable)
defaults.someProperty = true
cancellable.removeAll()
PlaygroundPage.current.needsIndefiniteExecution = true
This prints:
Before: false
Sink: false
Sink: true
Sink: true
Why is it firing the sink 3 times instead of only once?
I can maybe understand it firing on subscribe, which is confusing because it doesn't seem to be a PassthroughSubject
or any documentation of this. However, what really confuses me is the third time it fires.
UPDATE:
It's strange but it seems the initial value gets factored into the new/old comparison:
defaults.someProperty = false
defaults.someProperty = true
defaults.someProperty = false
defaults.someProperty = true
print("Initial: \(defaults.someProperty)")
defaults
.publisher(for: \.someProperty, options: [.new])
.sink { print("Sink: \($0)") }
.store(in: &cancellable)
defaults.someProperty = true
The above will print which looks good:
Initial: true
Sink: true
But when the initial value is different than what you set it to:
defaults.someProperty = false
defaults.someProperty = true
defaults.someProperty = false
defaults.someProperty = true
defaults.someProperty = false
print("Initial: \(defaults.someProperty)")
defaults
.publisher(for: \.someProperty, options: [.new])
.sink { print("Sink: \($0)") }
.store(in: &cancellable)
defaults.someProperty = true
The above will strangely print:
Initial: false
Sink: true
Sink: true
This is untiutive because it's treating the initial value as a trigger of [.new]
, then compares again for what was set.
The first published value is the initial value when you subscribe, if you don't want to receive the initial value you can specify this in options (they are NSKeyValueObservingOptions
):
defaults
.publisher(for: \.someProperty, options: [.new])
.sink { print("Sink: \($0)") }
.store(in: &cancellable)
Every new value is indeed published twice, but you can just remove duplicates:
defaults
.publisher(for: \.someProperty, options: [.new])
.removeDuplicates()
.sink { print("Sink: \($0)") }
.store(in: &cancellable)
Which will give you the behaviour you want.
UPDATE:
if you define your extension like this:
extension UserDefaults {
@objc var someProperty: Bool {
bool(forKey: "someProperty")
}
}
and then set the value using:
defaults.set(false, forKey: "someProperty")
The values are published only once.