0
class ViewController: NSViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        print("in viewDidLoad");
        // addObserver keyPath
        UserDefaults.standard.addObserver(self, forKeyPath: "testKey", options: .new, context: nil);

        print("out viewDidLoad");
    }

    deinit {
        // removeObserver keyPath
        UserDefaults.standard.removeObserver(self, forKeyPath: "testKey");
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("in observeValue keyPath: \(keyPath) value: \(UserDefaults.standard.integer(forKey: "testKey"))");
        // 1. If I execute the func click () method, it will be executed two times
        // 2. If App originally existed "testKey", then func observeValue () will be executed after the viewDidLoad is finished.
    }

    @IBAction func click(_ sender: NSButton) {
        UserDefaults.standard.set(arc4random(), forKey: "testKey");
    }
}

The above code is all of my test code. I used KVO in my own project, but found repeated execution.

// 1. If I execute the func click () method, it will be executed two times

// 2. If App originally existed "testKey", then func observeValue () will be executed after the viewDidLoad is finished.

This is not what I understand about KVO. My idea is that after addObserver, my observeValue will be called if my key is changed. But it didn't turn out that way. I tried to find the answer to the forum, and I didn't find the answer. I just found a similar question.

If I press Button in my view, then the final result will be..:

in viewDidLoad
out viewDidLoad
in observeValue keyPath: Optional("testKey") value: 4112410111
in observeValue keyPath: Optional("testKey") value: 3712484288
in observeValue keyPath: Optional("testKey") value: 3712484288

macos: 10.12.6 (16G29) xcode: 9 beta6、xcode 8.3.3

If you have the same problem, please tell more people to help us solve it. Thank you

I have sent the same question to the official, and if there is a solution, I will return it here.

Simon
  • 438
  • 4
  • 14
  • Same question here: https://stackoverflow.com/questions/45266733/observevalue-called-twice-on-userdefaults, unfortunately without a solution. – Martin R Sep 13 '17 at 17:55
  • Looking forward to solving the problem, this is not in line with expectations. – Simon Sep 13 '17 at 18:24
  • It is a bug, and fixed in iOS 11/macOS 10.13, see https://stackoverflow.com/a/45464568/1187415. – Martin R Sep 14 '17 at 18:55

1 Answers1

0

From setting a breakpoint in observeValue() and looking at the trace, it appears that the observations are getting fired in two places; one during click() as an effect of the line where you tell UserDefaults to set the value, and another later on, scheduled on the run loop so it happens after click() has already returned, when the system detects that the value has changed. This double notification could probably be considered a bug, since the latter notification should render the former unnecessary, and I'd consider filing a radar report on it.

Unfortunately, I can't see any way to disable this behavior. I can think of a workaround, but it's extremely hacky, kludgey, ugly, and I probably wouldn't actually do it unless the need is absolutely dire. But here it is:

private var kvoContext = 0

private let ignoreKVOKey = "com.charlessoft.example.IgnoreKVO"

// If this can be called from a thread other than the main thread,
// then you will need to take measures to protect it against race conditions
private var shouldIgnoreKVO = false

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if context == &self.kvoContext { // always, always use a context pointer
        if !shouldIgnoreKVO { // if this is a notification we don't want, ignore it
            print("in observeValue keyPath: \(String(describing: keyPath)) value: \(UserDefaults.standard.integer(forKey: "testKey"))");
        }
    } else {
        // call super if context pointer doesn't match ours
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
    }
}

@IBAction func click(_ sender: NSButton) {
    // we don't need this notification, since we'll get the later one
    // resulting from the defaults having changed
    self.shouldIgnoreKVO = true
    defer { self.shouldIgnoreKVO = false }

    UserDefaults.standard.set(arc4random(), forKey: "testKey");
}

Again, it's ugly, it's hacky, I probably wouldn't actually do it. But there it is.

Charles Srstka
  • 16,665
  • 3
  • 34
  • 60