UserDefaults.didChangeNotification not firing

Hi,

I'm currently working on an app made originally for iOS 15. On it, I add an observer on viewDidLoad function of my ViewController to listen for changes on the UserDefault values for connection settings.

NotificationCenter.default.addObserver(self, selector: #selector(settingsChanged), name: UserDefaults.didChangeNotification, object: nil)

Said values can only be modified on the app's section from System Settings.

Thing is, up to iOS 17, the notification fired as expected, but starting from iOS 18, the notification doesn't seem to be sent by the OS.

Is there anything I should change in my observer, or any other technique to listen for the describe event?

Thanks in advance.

Answered by DTS Engineer in 805946022

Hmmm, this is tricky. The docs for this notification are pretty clear about its semantics:

This notification isn't posted when changes are made outside the current process, or when ubiquitous defaults change.

And Settings is definitely “outside the current process”.

Having said that, this is a pretty common case and it’s probably worth filing a bug about it. Please post your bug number, just for the record.

In the meantime, the solution is to use KVO to monitor for changes. Quoting the docs again:

You can use key-value observing to register observers for specific keys of interest in order to be notified of all updates, regardless of whether changes are made within or outside the current process.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

Hmmm, this is tricky. The docs for this notification are pretty clear about its semantics:

This notification isn't posted when changes are made outside the current process, or when ubiquitous defaults change.

And Settings is definitely “outside the current process”.

Having said that, this is a pretty common case and it’s probably worth filing a bug about it. Please post your bug number, just for the record.

In the meantime, the solution is to use KVO to monitor for changes. Quoting the docs again:

You can use key-value observing to register observers for specific keys of interest in order to be notified of all updates, regardless of whether changes are made within or outside the current process.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Can you post an example how to do this? Thx ;)

Find a solution:

userDefaults.addObserver(self, forKeyPath: "youKey", options: .new, context: nil)

And:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

    //do something 

}

If you’re using Swift then it has a much nicer interface to KVO, namely the observe(_:options:changeHandler:) method. For example, if I add code like this:

extension UserDefaults {
    @objc dynamic var myProperty: String? {
        get {
            self.string(forKey: "myProperty")
        }
        set {
            self.set(newValue, forKey: "myProperty")
        }
    }
}

to expose the defaults value as a property, I can then write code like this:

class MyClass {
    var myPropertyObservation: NSKeyValueObservation?

    func startObserving() {
        let defaults = UserDefaults.standard
        self.myPropertyObservation = defaults.observe(\.myProperty, options: [.new, .old]) { _, change in
            print(change)
        }
    }
}

I then modify the defaults value using defaults:

% defaults write "com.example.apple-samplecode.Test764675" "myProperty" "some value"

and the change handler runs and prints:

NSKeyValueObservedChange<Optional<String>>(kind: __C.NSKeyValueChange, newValue: Optional(Optional("some value")), oldValue: Optional(nil), indexes: nil, isPrior: false)

Nice!

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi Quinn, sadly this doesn't work for my SwiftUI iOS app. I added the startObserving() routine to my controller class, and called it from init, but it neither triggers the print(change) closure when I switch to iOS Settings->Apps->myApp and toggle the Toggle, nor when I return back to my app... Is this because the UserDefaults property is a string and my Settings.bundle Root.plist defines a PSToggleSwitchSpecifier?

How can I observe a Toggle (instead of a string)?

OK, got it running:

extension UserDefaults {
    private static let myBoolKey = "myBool"
    @objc dynamic var myBool: Bool {
        get { bool(forKey: UserDefaults.myBoolKey) }
        set { set(newValue, forKey: UserDefaults.myBoolKey) }
    }
}

(note that the value of the key "myBool" MUST BE identical to the name of the dynamic myBool, otherwise it doesn't work)

and then call it in the init function of the (singleton) class:

        self.myBoolObservation = UserDefaults.standard.observe(\.myBool, options: [.new, .old,.prior,.initial]) {  [weak self](_, _) in
            self?.myBool = UserDefaults.standard.myBool
        }

However, this works only once on iOS 18, or rather only while the app keeps running. When you kill the app and launch it again, the closure is never called again. On iOS 17 I can quit and relaunch my app, and this code continues to work fine...

Thus, still the problem of the thread starter (aolguin) -and mine too- is unsolved: since iOS 18 we don't get notifications in the app when the user changes app settings in Settings->Apps->myApp.

How exactly are you testing this on iOS? Can you post a step-by-step explanation of the process?

On macOS it’s easy to test this because multitasking was built in to that platform from day one. Indeed, the Test764675 test I posted earlier relies on that [1].

On iOS that’s not the case, so there are a bunch of different ways you can approach this. I’d like to get a better handle on exactly what you’re doing so that I can try it for myself.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] And FYI, I repeated that test just not, on macOS 15.3.1, and it continues to work as expected.

UserDefaults.didChangeNotification not firing
 
 
Q