Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does User Defaults publisher trigger multiple times

Tags:

swift

combine

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.

like image 652
TruMan1 Avatar asked Oct 15 '25 08:10

TruMan1


1 Answers

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.

like image 69
LuLuGaGa Avatar answered Oct 18 '25 04:10

LuLuGaGa



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!