How to use UserDefaults.publisher in SwiftUI

UserDefaults.publisher works really well in UIKit, and even Apple's documentation mainly revolves around that. However, I'm unable to get the desired behaviour of continous observing of values using UserDefaults.publisher in SwiftUI.

Example UIKit Code that works perfectly:

import UIKit
import Combine
  
extension UserDefaults {
    @objc dynamic var test: Int {
        return integer(forKey: "test")
    }
}
  
class ViewController: UIViewController {
    var subscriber: AnyCancellable?   // Subscriber of preference changes.
  
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        subscriber = UserDefaults.standard
        .publisher(for: \.test)
            .sink() {
                print($0)
        }
        
        UserDefaults.standard.set(5, forKey: "test")
        UserDefaults.standard.set(10, forKey: "test")
        UserDefaults.standard.set(100, forKey: "test")
    }
}

Output of above code:
[initial value]
5
10
100
Problematic SwiftUI code:

import SwiftUI
import Combine

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

struct ContentView: View {
    //    @ObservedObject var auth = AuthModel()
    @State var cancellable: AnyCancellable? = UserDefaults.standard
        .publisher(for: \.userValue)
        .sink() {
            print($0)
    }
   
    var body: some View {
         Text("hello!") 
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Output: prints the value stored once, however, after that, it doesn't reprints the values automatically on changes.


I've tried using a separate Model class, but the issue stays and the core reason remains the same I guess.


Any help would be greatly appreciated, thanks!

Answered by OOPer in 423610022

Thanks for updating your code. And I can confirm that your updated UIKit code works as you describe.


The reason these changes fix the issue is because `publisher(for:)` is a method for NSObject and works with KVO-compliant properties.

UserDefaults works as KVO-compliant for the keys specified in `set(_:forKey:)`.

So, if you want to obseve updates made with `set(..., forKey: "test")`, you need to observe on the ObjC key path "test".


Please apply the same fix to your SwiftUI code and see what happens.

If it still shows unexpected behavior, please show whole updated code including the code which sets value to UserDefaults.

Your UIKit code shows only [initial value], and no outputs follow. (And that's what I expect.)


Maybe your code shown is not the same as the code tested.


Please show the right code to reproduce the issue you described.

Oh yes. I've updated the code, please check now. Thank you for your help!
Change in code:

Changed key string and variable name in UserDefault extension. I don't know why this matters, but apparently it does.

Accepted Answer

Thanks for updating your code. And I can confirm that your updated UIKit code works as you describe.


The reason these changes fix the issue is because `publisher(for:)` is a method for NSObject and works with KVO-compliant properties.

UserDefaults works as KVO-compliant for the keys specified in `set(_:forKey:)`.

So, if you want to obseve updates made with `set(..., forKey: "test")`, you need to observe on the ObjC key path "test".


Please apply the same fix to your SwiftUI code and see what happens.

If it still shows unexpected behavior, please show whole updated code including the code which sets value to UserDefaults.

We don't use @State for publishers. Try this instead:

Code Block
struct ContentView: View {
// @ObservedObject var auth = AuthModel()
let publisher = UserDefaults.standard.publisher(for: \.userValue)
var body: some View {
Text("hello!")
.onReceive(publisher) { newUserValue in
print(newUserValue)
}
}
}


However if you actually want to use the value in your view then it is simply:

Code Block
struct ContentView2: View {
    @AppStorage("userValue")
private var userValue = ""
    var body: some View {
        Text("hello! " + userValue)
    }
}

is there a more dynamic way of doing this? adding a new extension on user defaults every time you have a new key isn't very scalable.

How to use UserDefaults.publisher in SwiftUI
 
 
Q