This might be an awful bug in iOS 9.3 (release).
When adding a single observer to [NSUserDefaults standardUserDefaults] I've noticed that the responding method -observeValueForKeyPath:ofObject:change:context: is called multiple times.
In the simple example below, every time a UIButton is pressed once, observeValueForKeyPath fires twice. In more complicated examples it fires even more times. It is only present on iOS 9.3 (both on sim and devices).
This can obviously wreak havoc on an app. Anyone else experiencing the same?
// ViewController.m (barebones, single view app)
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"viewDidLoad");
[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:#"SomeKey" options:NSKeyValueObservingOptionNew context:NULL];
}
- (IBAction)buttonPressed:(id)sender {
NSLog(#"buttonPressed");
[[NSUserDefaults standardUserDefaults] setInteger:1 forKey:#"SomeKey"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
NSLog(#"observeValueForKeyPath: %#", keyPath);
}
Yes I am experiencing this as well and it seems to be a bug, below is a quick workaround I’m using for the moment until this is fixed. I hope it helps!
Also to clarify, since iOS 7 KVO has been working great with NSUserDefaults and it certainly appears to be key value observable as Matt stated, it is explicitly written in NSUserDefaults.h in the iOS 9.3 SDK: “NSUserDefaults can be observed using Key-Value Observing for any key stored in it."
#include <mach/mach.h>
#include <mach/mach_time.h>
#property uint64_t newTime;
#property uint64_t previousTime;
#property NSString *previousKeyPath;
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
//Workaround for possible bug in iOS 9.3 SDK that is causing observeValueForKeyPath to be called multiple times.
newTime = mach_absolute_time();
NSLog(#"newTime:%llu", newTime);
NSLog(#"previousTime:%llu", previousTime);
//Try to avoid duplicate calls
if (newTime > (previousTime + 5000000.0) || ![keyPath isEqualToString:previousKeyPath]) {
if (newTime > (previousTime + 5000000.0)) {
NSLog(#"newTime > previousTime");
previousTime = newTime;
NSLog(#"newTime:%llu", newTime);
NSLog(#"previousTime:%llu", previousTime);
}
if (![keyPath isEqualToString:previousKeyPath]) {
NSLog(#"new keyPath:%#", keyPath);
previousKeyPath = keyPath;
NSLog(#"previousKeyPath is now:%#", previousKeyPath);
}
//Proceed with handling changes
if ([keyPath isEqualToString:#“MyKey"]) {
//Do something
}
}
}
When adding a single observer to [NSUserDefaults standardUserDefaults] I've noticed that the responding method -observeValueForKeyPath:ofObject:change:context: is called multiple times
This is a known issue and is reported (by Apple) as fixed in iOS 11 and macOS 10.13.
Adding this answer for MacOS (10.13) which definitely has the bug getting multiple notifications for KVO of NSUserDefault Keys, and which also addresses deprecations. It is better to use a calculation for elapsed nano seconds that gets it for the machine you are running on. Do it like so:
#include <mach/mach.h>
#include <mach/mach_time.h>
static mach_timebase_info_data_t _sTimebaseInfo;
uint64_t _newTime, _previousTime, _elapsed, _elapsedNano, _threshold;
NSString *_previousKeyPath;
-(BOOL)timeThresholdForKeyPathExceeded:(NSString *)key thresholdValue:(uint64_t)threshold
{
_previousTime = _newTime;
_newTime = mach_absolute_time();
if(_previousTime > 0) {
_elapsed = _newTime - _previousTime;
_elapsedNano = _elapsed * _sTimebaseInfo.numer / _sTimebaseInfo.denom;
}
if(_elapsedNano > threshold || ![key isEqualToString:_previousKeyPath]) {
if(![key isEqualToString:_previousKeyPath]) _previousKeyPath = key;
return YES;
}
return NO;
}
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if(![self timeThresholdForKeyPathExceeded:keyPath thresholdValue:5000000]) return; // Delete this line of MacOS bug ever fixed
}
// Else this is the KeyPath you are looking for Obi Wan, process it.
}
This is based on Listing 2 of this Apple Doc:
https://developer.apple.com/library/content/qa/qa1398/_index.html
Related
I'm using a solution for here to make titleView clipsToBounds always true.
I have this in my ViewController and it works well, however, if I leave the ViewController by pressing the back button and then come back, it app crashes at the dispatch_async line.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if([object isEqual:[[self.navigationController.navigationBar subviews] objectAtIndex:2]]) {
dispatch_async(dispatch_get_main_queue(), ^{
[[self.navigationController.navigationBar subviews] objectAtIndex:2].clipsToBounds = NO;
[self.navigationItem.titleView layoutIfNeeded];
});
}
}
Edit:
The only error I get is: Thread 1: EXC_BAD_ACCESS (code=1, address=0x102d8860)
The console doesn't provide any information other than (lldb)
You must removeObserver if you go out from viewController.
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.navigationController.navigationBar.subviews[2]
removeObserver:self
forKeyPath:#"clipsToBounds"];
}
Check to your block
if (self.navigationController.navigationBar.subviews.count > 1){
…
This might be an awful bug in iOS 9.3 (release).
When adding a single observer to [NSUserDefaults standardUserDefaults] I've noticed that the responding method -observeValueForKeyPath:ofObject:change:context: is called multiple times.
In the simple example below, every time a UIButton is pressed once, observeValueForKeyPath fires twice. In more complicated examples it fires even more times. It is only present on iOS 9.3 (both on sim and devices).
This can obviously wreak havoc on an app. Anyone else experiencing the same?
// ViewController.m (barebones, single view app)
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"viewDidLoad");
[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:#"SomeKey" options:NSKeyValueObservingOptionNew context:NULL];
}
- (IBAction)buttonPressed:(id)sender {
NSLog(#"buttonPressed");
[[NSUserDefaults standardUserDefaults] setInteger:1 forKey:#"SomeKey"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
NSLog(#"observeValueForKeyPath: %#", keyPath);
}
Yes I am experiencing this as well and it seems to be a bug, below is a quick workaround I’m using for the moment until this is fixed. I hope it helps!
Also to clarify, since iOS 7 KVO has been working great with NSUserDefaults and it certainly appears to be key value observable as Matt stated, it is explicitly written in NSUserDefaults.h in the iOS 9.3 SDK: “NSUserDefaults can be observed using Key-Value Observing for any key stored in it."
#include <mach/mach.h>
#include <mach/mach_time.h>
#property uint64_t newTime;
#property uint64_t previousTime;
#property NSString *previousKeyPath;
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
//Workaround for possible bug in iOS 9.3 SDK that is causing observeValueForKeyPath to be called multiple times.
newTime = mach_absolute_time();
NSLog(#"newTime:%llu", newTime);
NSLog(#"previousTime:%llu", previousTime);
//Try to avoid duplicate calls
if (newTime > (previousTime + 5000000.0) || ![keyPath isEqualToString:previousKeyPath]) {
if (newTime > (previousTime + 5000000.0)) {
NSLog(#"newTime > previousTime");
previousTime = newTime;
NSLog(#"newTime:%llu", newTime);
NSLog(#"previousTime:%llu", previousTime);
}
if (![keyPath isEqualToString:previousKeyPath]) {
NSLog(#"new keyPath:%#", keyPath);
previousKeyPath = keyPath;
NSLog(#"previousKeyPath is now:%#", previousKeyPath);
}
//Proceed with handling changes
if ([keyPath isEqualToString:#“MyKey"]) {
//Do something
}
}
}
When adding a single observer to [NSUserDefaults standardUserDefaults] I've noticed that the responding method -observeValueForKeyPath:ofObject:change:context: is called multiple times
This is a known issue and is reported (by Apple) as fixed in iOS 11 and macOS 10.13.
Adding this answer for MacOS (10.13) which definitely has the bug getting multiple notifications for KVO of NSUserDefault Keys, and which also addresses deprecations. It is better to use a calculation for elapsed nano seconds that gets it for the machine you are running on. Do it like so:
#include <mach/mach.h>
#include <mach/mach_time.h>
static mach_timebase_info_data_t _sTimebaseInfo;
uint64_t _newTime, _previousTime, _elapsed, _elapsedNano, _threshold;
NSString *_previousKeyPath;
-(BOOL)timeThresholdForKeyPathExceeded:(NSString *)key thresholdValue:(uint64_t)threshold
{
_previousTime = _newTime;
_newTime = mach_absolute_time();
if(_previousTime > 0) {
_elapsed = _newTime - _previousTime;
_elapsedNano = _elapsed * _sTimebaseInfo.numer / _sTimebaseInfo.denom;
}
if(_elapsedNano > threshold || ![key isEqualToString:_previousKeyPath]) {
if(![key isEqualToString:_previousKeyPath]) _previousKeyPath = key;
return YES;
}
return NO;
}
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if(![self timeThresholdForKeyPathExceeded:keyPath thresholdValue:5000000]) return; // Delete this line of MacOS bug ever fixed
}
// Else this is the KeyPath you are looking for Obi Wan, process it.
}
This is based on Listing 2 of this Apple Doc:
https://developer.apple.com/library/content/qa/qa1398/_index.html
I am attempting to accessing related artists/playlists, etc using the SPSearch class as follows, but upon examining the logs it appears that the search never finishes loading:
self.search = [SPSearch searchWithSearchQuery:self.artist inSession:[SPSession sharedSession]];
[SPAsyncLoading waitUntilLoaded:self.search timeout:100.0 then:^(NSArray *loadedItems, NSArray *notLoadedItems){
NSLog(#"Search completed, Loaded items = %d, unloaded items = %d", [loadedItems count], [notLoadedItems count]);
}];
I saw a similar question SPSearch in CocoaLibSpotify but the Key-Value-observer appears to be overkill/convoluted for what I would like to do(maybe it is just my relative newness to iOS dev that makes the KVO seem as overkill/convoluted). Even if it isn't overkill, how would I go about using the SPSearch class to accomplish what I would like to do? It looks like it should be straight forward, but I appear to be getting hung up with some devilish details(search never loads, artist/playlists arrays returning count of of 0, etc).
Edit:
Additionally, I attempted to give the KVO a shot, as opposed to the SPAsyncLoading, but it it has been 10 minutes, and no change seems to have been observed:
[self addObserver:self
forKeyPath:#"search.loaded"
options:0
context:nil];
...
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:#"search.loaded"]) {
if (!self.search.isLoaded)
return;
NSLog(#"artists length %d", self.search.artists.count);
NSLog(#"playlistslength %d", self.search.playlists.count);
NSLog(#"albums length %d", self.search.albums.count);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
Also, I can rule out a network related issue, I am over Wifi with solid data rates and authentication happens flawlessly.
Thanks.
I've recently begun to discover what can be done with KVO and I'm refactoring some of my code and saving a lot of lines at the same time. I do face one issue that is so general that it makes me wonder whether a certain pattern is recommended.
In some cases I load a new view controller that needs to represent data from an already initialized model. On -viewDidLoad I would register for KVO:
[_model addObserver:self
forKeyPath:kSomeKey
options:NSKeyValueObservingOptionNew
context:(__bridge void *)(_model)];
and change my interface when values change:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqual:kSomeKey] && context == (__bridge void *)(_model)) {
[self updateSomeInterfaceElement];
}
Unfortunately and understandably, the view is not updated with current values from the model when I load my new view.
Is my best option to call -updateSomeInterfaceElement in -viewDidLoad? It doesn't seem to be a big deal like this, but when listening for 10-20 properties, it looks very inefficient (especially since all my -updateSomeInterfaceElement methods are mostly 1 line only, so no need to make them into a separate method). Is there any way to circumvent this, or is there a more elegant solution?
You want to change your options to include NSKeyValueObservingOptionInitial. This will cause KVO to fire a notification when you add the observer, providing the observer with the "initial" value.
Also, as an aside, you should get in the habit of calling super if observeValueForKeyPath:... is called for a notification you didn't sign up for. Also, it's a bit more bulletproof to avoid using "live" pointers in the role of KVO contexts (since a future object could have the same pointer if the current object is deallocated.) I generally prefer to use a pattern like this:
static void * const MyObservationContext = (void*)&MyObservationContext;
- (void)viewDidLoad
{
// ... other stuff ...
[self addObserver:self forKeyPath:#"model.someKey" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:MyObservationContext];
// ... other stuff ...
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == MyObservationContext)
{
// Do stuff
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
I had a property named myName in my class, like:
#property (nonatomic, strong) NSString *myName;
I need to send a notification when the myName property's value is changed.
Now I'm doing something like:
- (void)setMyName:(NSString *)name
{
_myName = name;
[[NSNotificationCenter defaultCenter] postNotificationName:CHANGE_NOTIFICATION object:nil];
}
I know there is something like Key-Value Observing in iOS. But I don't know how to implement it, I read the entire document, but couldn't get a good understanding.
Please help me to understand how to implement the same without using custom setter.
Try this:
MyClass *var = [MyClass new];
[var addObserver:self forKeyPath:#"myName" options:NSKeyValueChangeOldKey context:nil];
and implement
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
}
this method will be called anytime when myName property changes
In - (void)setMyName:(NSString *)name do this instead
[self willChangeValueForKey:#"myName"];
_myName = name;
[self didChangeValueForKey:#"myName"];
//this generates the KVO's
And where you want to listen (the viewController), there in viewDidLoad add this line:
[w addObserver:self forKeyPath:#"myName"
options:NSKeyValueObservingOptionNew context:nil];
//By doing this, you register the viewController for listening to KVO.
and also implement this method:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([[change objectForKey:NSKeyValueChangeNewKey] isEqual:[NSNull null]]) {
return;
} else {
//read the change dictionary, and have fun :)
}
}
//this method is invoked, whenever the property's value is changed.
To do this without the customer setter, just synthesize the property setter. This will create all the supporting calls to willChangeValueForKey / didChangeValueForKey.
#synthesize myName;
Then set property values with dot-syntax:
self.myName = #"Inigo Montoya"
Then the observers will receive the KVO notification automatically.
(You will need to remove the observer before you release the observed object.)