fragon
fragon

Reputation: 3471

iOS KVO - detect when the same value is set again

Is it possible to use KVO in a way that it detects not only if the value changed, but also if the same value was set again? I'm currently receiving notifications only when the value changed (is different from the previously set one). I need to receive notification every time the value is set (even if it's the same as the one previously set). How can I achieve this?

My code:

private func addObserver() {
    defaults.addObserver(self, forKeyPath: DefaultsKeys.testKey._key, options: .new, context: nil)
}

public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    guard let value = change?[NSKeyValueChangeKey.newKey] as? Bool else { return }
    statusCallback?(value)
}

private func removeObserver() {
    defaults.removeObserver(self, forKeyPath: DefaultsKeys.testKey._key)
}

Upvotes: 1

Views: 1368

Answers (4)

A. Petrov
A. Petrov

Reputation: 998

If you want to track every setting a value in NSUserDefaults, even if the new value is the same as previous, wrap the value with the NSDictionary, and put inside the dictionary an NSUUID value, generated every time as a new setValue being called.

Before (observeValueForKeyPath did not called on every setValue):

[self.mySharedDefaults setValue: @"CHECKING" forKey:@"appStatusOUT"];

After (observeValueForKeyPath being called on every setValue):

[self.mySharedDefaults setValue: [NSDictionary dictionaryWithObjectsAndKeys: @"CHECKING", @"CMD",
                          [NSUUID UUID].UUIDString, @"UUID", nil] forKey:@"appStatusOUT"];

Upvotes: -1

Hexfire
Hexfire

Reputation: 6058

KVO mechanism is very simple - it does not perform any additional checks upon setting a new value, it is merely triggered when a setter is called. Hence it is not possible to differentiate if the value is different from that which is already being set. And it's good. Firstly, because, it's not typical in practice to assign the same value to a variable.
Second, introducing additional checks would be consumable and in most cases unneeded. If that check existed, it would negatively affect performance.

That being said, as far as Swift is concerned, you can consider replacing KVO-mechanism (essentially an Objective-C legacy) with native Swift property observers: willSet and didSet. And this would essentially play the same role as by passing both options: NSKeyValueObservingOptionNew and NSKeyValueObservingOptionOld (.old and .new in Swift) to addObserver method. Once having specified these flags, whenever KVO mechanism is triggered, you will receive both values (old an new) in observeValue(...), from where you can decide what to do with either. But why would you need such complexity when willSet does practically the same and is much more conveinent:

var myVariable : String! {
    willSet {
        print("Old value is: \(myVariable)")
        print("New value is: \(newValue)")

        // Let's do something with our old value
    }
    didSet {
        print("Just set the new value: \(newValue)")

        // New value is set. Let's do some actions.
    }
}

Upvotes: 0

Rob
Rob

Reputation: 437412

KVO generally is called every time the observed property is set, even if it's the same value it was last time. But I guess you're observing UserDefaults, and which has an idiosyncrasy that prevents this from happening (probably an optimization that prevents unnecessary saves of the store).

You can register for .didChangeNotification, which appears to called whether the value changed or not:

NotificationCenter.default.addObserver(forName: UserDefaults.didChangeNotification, object: nil, queue: .main) { notification in
    print("notification", notification)
}

Upvotes: 2

atereshkov
atereshkov

Reputation: 4555

You can achieve this by doing like:

public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
   let value = change?[.oldKey] as? Bool
   guard value == nil || value != yourVariableToCheck else { return }
   statusCallback?(value)
}

Only change yourVariableToCheck on your own variable.

Upvotes: 0

Related Questions