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
How to Replace My .Xib File with Pure Swift 3
Upload Multiple Images to Ftp Server in iOS
Pass Custom Parameter to UIbutton #Selector Swift 3
Does Realm Support Computed Property in Swift
Swift Casting Generic to Optional with a Nil Value Causes Fatalerror
Passing Values Between Viewcontrollers Based on List Selection in Swift
Xcode8 Beta 6 - Urlsession with Completionhandler Argument Not Working
How We Can Read and Write to Same Observableobject in Swiftui
Xcode + Swift + Darwin.Ncurses = "A_Bold Not Found" Compilation Error. I Can't Get Bright Colors
Compiling for iOS 10.3, But Module 'swiftuicharts' Has a Minimum Deployment Target of iOS 13.0
Parse Migration Error to Mlabs and Heroku
How to Specify The Name of The Output Executable
Swift Bug? Calling Super Class Method When Subclass with Generic Type
Swift: Switch Statement Fallthrough Behavior
Typecase Regular Swift Function to Curry Function
Adding Items to The Dock Menu from My View Controller in My Cocoa App