Why Does User Defaults Publisher Trigger Multiple Times

Why does User Defaults publisher trigger multiple times

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.

Cannot get UserDefautls updated values in realtime

The possible solution is to activate ObservableObject publisher explicitly in every setter, like below

var userCity : String {
get {
return self.userSettings.settings.string(forKey: "userCity") ?? ""
}
set {
self.userSettings.settings.set(newValue, forKey: "userCity")
self.objectWillChange.send() // << this one !!
}
}

UserDefaults insanity, take 2 with a DatePicker

here is my 2nd answer, if you want to update the text also...it is not "nice" and for sure not the best way, but it works (i have tested it)

struct ContentView: View {

let defaults = UserDefaults.standard

var dateFormatter: DateFormatter {
let formatter = DateFormatter()
// formatter.dateStyle = .long
formatter.dateFormat = "dd MMM yy"
return formatter
}
@State var uiUpdate : Int = 0
@State var selectedDate : Date
@State var oldDate : Date = Date()

init() {
_selectedDate = State(initialValue: UserDefaults.standard.object(forKey: "MyDate") as! Date) // This should set "selectedDate" to the UserDefaults value
}

var body: some View {

VStack {
Text("The BOOL 1 value is : Bool 1 = \(String(defaults.bool(forKey: "MyBool 1")))")
Divider()
Text("My string says : \(String(defaults.string(forKey: "MyString")!))")
Divider()
Text("The date from UserDefaults is : \(dateFormatter.string(from: defaults.object(forKey: "MyDate") as! Date))")
.background(uiUpdate < 5 ? Color.yellow : Color.orange)
Divider()
DatePicker(selection: $selectedDate, label: { Text("") })
.labelsHidden()
.onReceive([self.selectedDate].publisher.first()) { (value) in
if self.oldDate != value {
self.oldDate = value
self.saveDate()
}
}

Divider()
Text("The chosen date is : \(selectedDate)")
}
}

func loadData() {
selectedDate = defaults.object(forKey: "MyDate") as! Date
print("----> selected date in \"init\" from UserDefaults: \(dateFormatter.string(from: selectedDate) )) ")
}

private func saveDate() { // This func is called whenever the Picker sends out a value
UserDefaults.standard.set(selectedDate, forKey: "MyDate")
print("Saving the date to User Defaults : \(dateFormatter.string(from: selectedDate) ) ")
uiUpdate = uiUpdate + 1
}

}

Creating a publisher that collects values after a trigger

One solution to this would be to employ the drop(untilOutputFrom:) and prefix(untilOutputFrom:) operators.

Consider the playground below.

In the code I create a signal startEmitting that doesn't emit anything until it sees the value 5. Then it emits a "true" value and stops.

Similarly the signal stopEmitting waits until it sees a 9 and when it does it emits a single value and stops.

The combined signal drops every value until startEmitting fires, and using prefix only emits values until stopEmitting fires. The net effect is that the combined signal only starts when a 5 is emitted and stops when 9 is emitted.

import UIKit
import Foundation
import Combine

var subj = PassthroughSubject<Int, Never>()

for iter in 0...10 {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(iter)) {
print("send \(iter)")
subj.send(iter)

if iter == 10 {
subj.send(completion: .finished)
}
}
}

let startEmitting = subj
.print("startEmtting")
.contains(5)

let stopEmitting = subj
.print("stopEmitting")
.contains(9)

let cancellable = subj
.drop(untilOutputFrom: startEmitting)
.prefix(untilOutputFrom: stopEmitting)
.collect()
.sink {
print("finish")
print($0)
} receiveValue: {
print($0)
}

How to use KVO for UserDefaults in Swift?

As of iOS 11 + Swift 4, the recommended way (according to SwiftLint) is using the block-based KVO API.

Example:

Let's say I have an integer value stored in my user defaults and it's called greetingsCount.

First I need to extend UserDefaults with a dynamic var that has the same name as the user defaults key you want to observe:

extension UserDefaults {
@objc dynamic var greetingsCount: Int {
return integer(forKey: "greetingsCount")
}
}

This allows us to later on define the key path for observing, like this:

var observer: NSKeyValueObservation?

init() {
observer = UserDefaults.standard.observe(\.greetingsCount, options: [.initial, .new], changeHandler: { (defaults, change) in
// your change logic here
})
}

And never forget to clean up:

deinit {
observer?.invalidate()
}

How to observe changes in UserDefaults?

Here is possible solution

import Combine

// define key for observing
extension UserDefaults {
@objc dynamic var status: String {
get { string(forKey: "status") ?? "OFFLINE" }
set { setValue(newValue, forKey: "status") }
}
}

class Station: ObservableObject {
@Published var status: String = UserDefaults.standard.status {
didSet {
UserDefaults.standard.status = status
}
}

private var cancelable: AnyCancellable?
init() {
cancelable = UserDefaults.standard.publisher(for: \.status)
.sink(receiveValue: { [weak self] newValue in
guard let self = self else { return }
if newValue != self.status { // avoid cycling !!
self.status = newValue
}
})
}
}

Note: SwiftUI 2.0 allows you to use/observe UserDefaults in view directly via AppStorage, so if you need that status only in view, you can just use

struct SomeView: View {
@AppStorage("status") var status: String = "OFFLINE"
...


Related Topics



Leave a reply



Submit