PDA

View Full Version : KVO Question.




DavidBlack
Aug 11, 2013, 07:11 PM
Hi guys I have this app that when my nsuserfaults change I want to detect that change and execute some code so I came across Key-Value-Observing. It works fine and all but I need to way to observe more than one key. This is the code I am using:

[[NSUserDefaults standardUserDefaults] addObserver:self
forKeyPath:@"On/Off"
options:NSKeyValueObservingOptionNew
context:NULL];

And this method is called whenever the user defaults is changed. Is there a way to observe multiple keys. Thanks a lot in advance.
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
}



iphonedude2008
Aug 11, 2013, 09:00 PM
I just use nsnotificationcenter. Its basically the same thing, but you have to send the notification. Plus, you do have to implement a weird method or worry about thread safety since you are doing it all yourself.

DavidBlack
Aug 11, 2013, 09:31 PM
I just use nsnotificationcenter. Its basically the same thing, but you have to send the notification. Plus, you do have to implement a weird method or worry about thread safety since you are doing it all yourself.

Thanks can you give me an example?

DavidBlack
Aug 11, 2013, 10:13 PM
I did some reading on KVO and it doesn't allow passing a custom selector to be invoked. So I will have to use NSNotificationCenter.

iphonedude2008
Aug 11, 2013, 10:59 PM
in the class notifying

[[NSNotificationCenter defaultCenter]postNotificationName: @"nameofnotification" object: nil];

in the class being notified

[[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(nameofmethodyouchoosetorespondthatreturnsvoidwithnoarguments) name:@"nameofnotification" object: nil];

smallduck
Aug 12, 2013, 12:09 PM
I did some reading on KVO and it doesn't allow passing a custom selector to be invoked. So I will have to use NSNotificationCenter.

To use NSNotificationCenter to detect changes to NSUserDefaults, you have to observe NSUserDefaultsDidChangeNotification, but there's no way to tell which of the keys was changed. See RootViewController.m in the AppPrefs sample code.

Apparently in recent iOS & OS X you can indeed use KVO to observe [NSUserDefaults standardUserDefaults] (see this StackOverflow answer (http://stackoverflow.com/a/10784822/592739)). Then to solve your original question and your objection quoted above, I can't recommend highly enough the KVO-wrapper by Mike Ash, MAKVONotificationCenter. It lets you provide a custom selector (or block!) for each observation and also allows passing an array of observation key strings. Plus it eliminates the need to manually remove the observation, they get removed automatically when the either the observed or observing object is deallocated. The github repo is here (https://github.com/mikeash/MAKVONotificationCenter), the articles about it are here (http://www.mikeash.com/pyblog/key-value-observing-done-right.html) and here (http://www.mikeash.com/pyblog/friday-qa-2012-03-02-key-value-observing-done-right-take-2.html).

Those articles don't give you a step-by-step guide for using MAKVONotificationCenter though (they're more about the details of its implementation). Here's what your setup code would be:
[[MAKVONotificationCenter defaultCenter] observeTarget:[NSUserDefaults standardUserDefaults]
keyPath:userdefaultsKey
selector:@selector(userdefaultsKey1Changed:ofObject:change:userInfo:)
userInfo:NULL options:NSKeyValueObservingOptionNew]; or with an array of keys in place of the single key string:
[[MAKVONotificationCenter defaultCenter] observeTarget:[NSUserDefaults standardUserDefaults]
keyPath:@[userdefaultsKey1, userdefaultsKey2]
selector:@selector(userdefaultsKey1or2Changed:ofObject:change:userInfo:)
userInfo:NULL options:NSKeyValueObservingOptionNew];

And the observation method:
- (void)userdefaultsKey1or2Changed(NSString *)keyPath ofObject:(id)target change:(NSDictionary *)change userInfo:(id)userInfo {
// do something with [[NSUserDefaults standardUserDefaults] objectForKey:keyPath]
// or [change objectForKey:NSKeyValueChangeNewKey]
}

I'll let you figure out the blocks API yourself, except for this gotcha: Although I rarely find myself needing either option flags NSKeyValueChangeNewKey or NSKeyValueChangeOldKey, note that the block doesn't get access to the change dictionary itself. Instead, to access the NSKeyValueChangeNewKey and NSKeyValueChangeOldKey values you instead use notification.newValue and notification.oldValue, where notification is the block's parameter.

DavidBlack
Aug 12, 2013, 12:43 PM
To use NSNotificationCenter to detect changes to NSUserDefaults, you have to observe NSUserDefaultsDidChangeNotification, but there's no way to tell which of the keys was changed. See RootViewController.m in the AppPrefs sample code.

Apparently in recent iOS & OS X you can indeed use KVO to observe [NSUserDefaults standardUserDefaults] (see this StackOverflow answer (http://stackoverflow.com/a/10784822/592739)). Then to solve your original question and your objection quoted above, I can't recommend highly enough the KVO-wrapper by Mike Ash, MAKVONotificationCenter. It lets you provide a custom selector (or block!) for each observation and also allows passing an array of observation key strings. Plus it eliminates the need to manually remove the observation, they get removed automatically when the either the observed or observing object is deallocated. The github repo is here (https://github.com/mikeash/MAKVONotificationCenter), the articles about it are here (http://www.mikeash.com/pyblog/key-value-observing-done-right.html) and here (http://www.mikeash.com/pyblog/friday-qa-2012-03-02-key-value-observing-done-right-take-2.html).

Those articles don't give you a step-by-step guide for using MAKVONotificationCenter though (they're more about the details of its implementation). Here's what your setup code would be:
[[MAKVONotificationCenter defaultCenter] observeTarget:[NSUserDefaults standardUserDefaults]
keyPath:userdefaultsKey
selector:@selector(userdefaultsKey1Changed:ofObject:change:userInfo:)
userInfo:NULL options:NSKeyValueObservingOptionNew]; or with an array of keys in place of the single key string:
[[MAKVONotificationCenter defaultCenter] observeTarget:[NSUserDefaults standardUserDefaults]
keyPath:@[userdefaultsKey1, userdefaultsKey2]
selector:@selector(userdefaultsKey1or2Changed:ofObject:change:userInfo:)
userInfo:NULL options:NSKeyValueObservingOptionNew];

And the observation method:
- (void)userdefaultsKey1or2Changed(NSString *)keyPath ofObject:(id)target change:(NSDictionary *)change userInfo:(id)userInfo {
// do something with [[NSUserDefaults standardUserDefaults] objectForKey:keyPath]
// or [change objectForKey:NSKeyValueChangeNewKey]
}

I'll let you figure out the blocks API yourself, except for this gotcha: Although I rarely find myself needing either option flags NSKeyValueChangeNewKey or NSKeyValueChangeOldKey, note that the block doesn't get access to the change dictionary itself. Instead, to access the NSKeyValueChangeNewKey and NSKeyValueChangeOldKey values you instead use notification.newValue and notification.oldValue, where notification is the block's parameter.

Thanks you very much for the awesome reply you really helped me. I used your code in a test project. Here is the code I used:
#import "AppDelegate.h"
#import "MAKVONotificationCenter.h"

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{

static NSString* const onekey = @"test";
static NSString* const twokey = @"test";
NSUserDefaults* defaultsQT = [NSUserDefaults standardUserDefaults];


[[MAKVONotificationCenter defaultCenter] observeTarget:[NSUserDefaults standardUserDefaults]
keyPath:@[onekey, twokey]
selector:@selector(userdefaultsKey1or2Changed:ofObject:change:userInfo:)
userInfo:NULL options:NSKeyValueObservingOptionNew];

// Insert code here to initialize your application
}

- (void)userdefaultsKey1or2Changed:(NSString *)keyPath ofObject:(id)target change:(NSDictionary *)change userInfo:(id)userInfo {
// do something with [[NSUserDefaults standardUserDefaults] objectForKey:keyPath]
// or [change objectForKey:NSKeyValueChangeNewKey]
}

And here is what I got in the debug area:

2013-08-12 13:37:21.204 Ob[9305:303] -[MAKVONotificationCenter userdefaultsKey1or2Changed:ofObject:change:userInfo:]: unrecognized selector sent to instance 0x107da1ba0
2013-08-12 13:37:21.206 Ob[9305:303] -[MAKVONotificationCenter userdefaultsKey1or2Changed:ofObject:change:userInfo:]: unrecognized selector sent to instance 0x107da1ba0
2013-08-12 13:37:21.208 Ob[9305:303] (
0 CoreFoundation 0x0000000101a8bb06 __exceptionPreprocess + 198
1 libobjc.A.dylib 0x00000001005a73f0 objc_exception_throw + 43
2 CoreFoundation 0x0000000101b2240a -[NSObject(NSObject) doesNotRecognizeSelector:] + 186
3 CoreFoundation 0x0000000101a7a02e ___forwarding___ + 414
4 CoreFoundation 0x0000000101a79e18 _CF_forwarding_prep_0 + 232
5 Ob 0x00000001000025c6 -[_MAKVONotificationHelper observeValueForKeyPath:ofObject:change:context:] + 214
6 Foundation 0x00000001000a07b7 NSKeyValueNotifyObserver + 390
7 Foundation 0x00000001000a21c1 NSKeyValueDidChange + 456
8 Foundation 0x000000010005d76a -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] + 118
9 CoreFoundation 0x0000000101b6d916 _CFPreferencesSetValueWithContainer + 262
10 Foundation 0x000000010008480d -[NSUserDefaults(NSUserDefaults) setObject:forKey:] + 90
11 AppKit 0x000000010076d834 -[NSUserDefaultsController _setSingleValue:forKey:] + 301
12 Foundation 0x00000001000e0925 -[NSObject(NSKeyValueCoding) setValue:forKeyPath:] + 292
13 AppKit 0x0000000100796de8 -[NSBinder _setValue:forKeyPath:ofObject:mode:validateImmediately:raisesForNotApplicableKeys:error:] + 274
14 AppKit 0x0000000100796c7d -[NSBinder setValue:forBinding:error:] + 248
15 AppKit 0x0000000100ddf43f -[NSValueBinder _applyObjectValue:forBinding:canRecoverFromErrors:handleErrors:typeOfAlert:discardEditingCallback:ot herCallback:callbackContextInfo:didRunAlert:] + 194
16 AppKit 0x0000000100ddf8dc -[NSValueBinder applyDisplayedValueHandleErrors:typeOfAlert:canRecoverFromErrors:discardEditingCallback:otherCallbac k:callbackContextInfo:didRunAlert:error:] + 612
17 AppKit 0x0000000100de0633 -[NSValueBinder performAction:] + 305
18 AppKit 0x0000000100e1c72f -[_NSBindingAdaptor _objectDidTriggerAction:bindingAdaptor:] + 133
19 AppKit 0x000000010091b79a -[NSControl sendAction:to:] + 56
20 AppKit 0x000000010091b6eb -[NSCell _sendActionFrom:] + 138
21 AppKit 0x0000000100919bd3 -[NSCell trackMouse:inRect:ofView:untilMouseUp:] + 1855
22 AppKit 0x0000000100919421 -[NSButtonCell trackMouse:inRect:ofView:untilMouseUp:] + 504
23 AppKit 0x0000000100918b9c -[NSControl mouseDown:] + 820
24 AppKit 0x000000010091050e -[NSWindow sendEvent:] + 6853
25 AppKit 0x000000010090c644 -[NSApplication sendEvent:] + 5761
26 AppKit 0x000000010082221a -[NSApplication run] + 636
27 AppKit 0x00000001007c6bd6 NSApplicationMain + 869
28 Ob 0x00000001000014e2 main + 34
29 libdyld.dylib 0x00000001040007e1 start + 0
)

ElectricSheep
Aug 12, 2013, 01:59 PM
I quickly browsed over the source code in the git repo for MAKVONotificationCenter. From what I gather, this:


[[MAKVONotificationCenter defaultCenter] observeTarget:[NSUserDefaults standardUserDefaults]
keyPath:@[onekey, twokey]
selector:@selector(userdefaultsKey1or2Changed:ofObject:change:userInfo:)
userInfo:NULL options:NSKeyValueObservingOptionNew];


is not the correct way to use -observeTarget:keyPath:selector:userInfo. Instead, you should probably do:


[self observeTarget:[NSUserDefaults standardUserDefaults]
keyPath:@[onekey, twokey]
selector:@selector(userdefaultsKey1or2Changed:ofObject:change:userInfo:)
userInfo:NULL options:NSKeyValueObservingOptionNew];


The reason is because -observeTarget is implemented as a category defined on NSObject (MAKVONotification). This means that every object will respond to these selectors, and its very easy to add yourself as an observer to any target.

DavidBlack
Aug 12, 2013, 02:04 PM
I quickly browsed over the source code in the git repo for MAKVONotificationCenter. From what I gather, this:


[[MAKVONotificationCenter defaultCenter] observeTarget:[NSUserDefaults standardUserDefaults]
keyPath:@[onekey, twokey]
selector:@selector(userdefaultsKey1or2Changed:ofObject:change:userInfo:)
userInfo:NULL options:NSKeyValueObservingOptionNew];


is not the correct way to use -observeTarget:keyPath:selector:userInfo. Instead, you should probably do:


[self observeTarget:[NSUserDefaults standardUserDefaults]
keyPath:@[onekey, twokey]
selector:@selector(userdefaultsKey1or2Changed:ofObject:change:userInfo:)
userInfo:NULL options:NSKeyValueObservingOptionNew];


The reason is because -observeTarget is implemented as a category defined on NSObject (MAKVONotification). This means that every object will respond to these selectors, and its very easy to add yourself as an observer to any target.]

Thank you so much I finally got it to work :)