Suppose i want to observe a property named 'isEnabled' on a property named 'controller' on self. AFAIK i have two options for installing such kind of observation:
1. [self.controller addObserver:self forKeyPath:#"isEnabled" options:0 context:nil];
2. [self addObserver:self forKeyPath:#"controller.isEnabled" options:0 context:nil];
I noticed the practical difference between the two approaches - on the second approach i will get a notification if the 'controller' object on self was replaced while with the first approach I will be notified only when 'isEnabled' property is changed on the same instance on which I installed the observation.
My question is where the hell is this documented if at all? I know it works but should I use it?
I could not find any mention of such behavior in Apple docs, though some other dudes mentioned it in forums. Any reference will be gladly accepted.
Thanks.
It's not just that you'll get a change notification if the controller property changes, it's that KVO will switch to tracking the isEnabled property of the new controller and stop tracking the isEnabled property of the old controller.
This is implicit in the notion of a key path. Key-Value Observing is built on top of Key-Value Coding. The Key-Value Coding Programming Guide says this about key paths in Key-Value Coding Fundamentals:
A key path is a string of dot separated keys that is used to specify a sequence of object properties to traverse. The property of the first key in the sequence is relative to the receiver, and each subsequent key is evaluated relative to the value of the previous property.
For example, the key path address.street would get the value of the address property from the receiving object, and then determine the street property relative to the address object.
The meaning of -addObserver:forKeyPath:options:context: is not "follow the key path to the final element and observe that property on the second-to-last object". It's "observe this key path of the receiver object". The key path is always considered starting from the receiver.
Put another way, for your second code example, it's not "observe the isEnabled property of the controller of self" (that's the meaning of your first example). The meaning is "observe the controller.isEnabled of self. Any time that the result of evaluating the expression [self valueForKeyPath:#"controller.isEnabled"] has or might have changed, notify me."
I know it works but should I use it?
Yes, you should use it. It's the intended meaning of the API. It's why the method describes its parameter as a key path rather than just a key.
on the second approach i will get a notification if the 'controller'
object on self was replaced
I would rephrase by saying that on the second approach you'll get notified if the controller object gets replaced or if its isEnabled property changes. In other word when controller.isEnabled changes (as explained by Ken's answer).
Check this example:
- (void)viewDidLoad {
[super viewDidLoad];
self.controller = [[ViewController2 alloc] init];
[self.controller addObserver:self forKeyPath:#"isEnabled" options:NSKeyValueObservingOptionNew context:nil];
[self addObserver:self forKeyPath:#"controller.isEnabled" options:NSKeyValueObservingOptionNew context:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.controller.isEnabled = !self.controller.isEnabled;
// replace controller
[self.controller removeObserver:self forKeyPath:#"isEnabled"];
self.controller = [[ViewController2 alloc] init];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.controller.isEnabled = !self.controller.isEnabled;
});
});
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(#"KVO %d %# %p", self.controller.isEnabled, keyPath, self.controller);
}
We'll get 4 KVO notifications:
KVO 1 controller.isEnabled 0x7fbbc2e4b4e0 <-- These 2 fire together when we toggle isEnbled
KVO 1 isEnabled 0x7fbbc2e4b4e0 <-- So basically 1. and 2. behave the same
KVO 0 controller.isEnabled 0x7fbbc2e58d30 <---- controller was replaced
KVO 1 controller.isEnabled 0x7fbbc2e58d30 <---- Toggle on the new instance
Related
Still not able to digest KVO with NSArray of objects. My requirement is e.g. suppose in a garage there are multiple cars. And I want to observe for change in car tyre properties like is the front tyre upgraded or the back one.
Car.h
#property(nonatomic, strong) NSString *frontTyre;
#property(nonatomic, strong) NSString *backTyre;
Garage.h
#property(nonatomic, strong) NSArray *cars; //Think there are two cars in the garage.
In CarOwner.m
[Garage addObserver:self forKeyPath:#"cars" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];]
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualToString:#"cars"])
{
//Here I am able to get the car object which got a type upgrade.
//But how to know which tyre got upgraded?
}
}
While upgrading the car tyre I am doing:
[self willChange:NSKeyValueChangeSetting valuesAtIndexes:index forKey:#"remoteUsers"];
[car1.frontTyre = #"upgraded"];
[self didChange:NSKeyValueChangeSetting valuesAtIndexes:index forKey:#"remoteUsers"];
Should I go this or this way? Or any other suggestion you may have, please.
You can use add observer as follow:
[Garage.cars addObserver:self toObjectsAtIndexes:[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, Garage.cars.count)] forKeyPath:#"frontTyre" options:0 context:nil];
Remember to remove observer when necessary.
After reading more about KVO understood that there is no way that one can observe properties of objects contained in a mutable array. We can observe properties of immutable array of objects through (as #zylenv mentioned)
[object addObserver:observer toObjectsAtIndexes:[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, array.count)] forKeyPath:#"property" options:0 context:nil];
However to observe mutable array of object's properties here are the steps:
Add observer to the array to get notified on object insertion and deletion operation.
When received a insertion notification, add an observer to the newly added object's properties that is of interest
And when a deletion notification is received simply remove the observer from the object that is added in step#2 (NOTE: the removed object will be in the "old" key of the change dictionary)
I have the following code which I'm using to try and observe a variable, and regenerate text when it changes. But so far nothing happens:/
-(void)viewdidload{
float Index = 1;//declared in header locally
[self addObserver:self forKeyPath:#"Index" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self generateAdviceText];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(#"inside");
if([keyPath isEqualToString:#"Index"]){
NSLog(#"INDEX CHANGED");
[self generateAdviceText];
}
}
-(void)generateAdviceText{
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *name = ((User *)appDelegate.users[self.currentUserIndex]).name;
NSString *clothWear = #"HAT";
NSMutableAttributedString *adviceString = [[NSMutableAttributedString alloc]initWithString:#"\"..."];
[adviceString appendAttributedString:[self boldString:name]];
[adviceString appendAttributedString:[self normalString:#", remember be extra aware of the cats today. The index is "]];
[adviceString appendAttributedString:[self boldString:[#(Index) stringValue]]];
[adviceString appendAttributedString:[self normalString:#", sth "]];
[adviceString appendAttributedString:[self boldString:clothWear]];
[adviceString appendAttributedString:[self normalString:#" sth "]];
[adviceString appendAttributedString:[self boldString:sth]];
[adviceString appendAttributedString:[self normalString:#"...\""]];
self.adviceLabel.attributedText = adviceString;
}
Can anybody spot my mistake? Thanks
Key-Value Observing does not generate change notifications for simple assignments to instance variables (or any other kind of variable).
In most cases, you need to call the setter of the property to generate a change notification for that object and that property.
At a low level, KVO only generates change notifications if the -willChange... and -didChange... methods from the NSKeyValueObserving informal protocol are called. Those methods are called in specific circumstances:
If a properly-named setter or collection mutation accessor for the property named by the key is called (assuming +automaticallyNotifiesObserversForKey: return true for that key). This is the best way.
If one of the -set... methods from the NSKeyValueCoding informal protocol, such as -setValue:forKey:, is called (again, assuming +automaticallyNotifiesObserversForKey: return true for that key).
If a collection proxy returned by one of the -mutable...ValueForKey: methods of the NSKeyValueCoding is sent mutation methods (again, depends on +automaticallyNotifiesObserversForKey:).
If some code (usually in the class defining the property) manually calls the -willChange... and -didChange... methods.
As Joe Shang noted, though, it's questionable for an object to observe itself. If an object wants to know when one of its properties has been changed, it should put the relevant code into its setter. Of course, it then has to modify its property exclusively using that setter, never by setting the instance variable directly (just like for KVO).
In the code you posted, though, it's just as well that you didn't get the KVO change notifications you expected. Your code would infinitely recurse until it crashed when it overflowed the stack. You attempted to make it so that changing the index calls -generateAdviceText and that -generateAdviceText changes the index.
What's the Index type? static variable or property? The keyPath in addObserver:forKeyPath:options:contexts: must be the property of some object (KVC compatible), you can read NSHipster's acticle and objc.io #7 to get more.
In the other hand, if Index is property, you don't need to use KVO here. You can override the setter method of Index and do some action when Index is update.
- (void)setIndex:(int)index
{
_index = index;
// add you generateAdviceText method here
}
I am seeing some random crashes with my app (although not reproducible when I run through same steps). I am observing the contentOffset property of the scrollview to take some action when it changes.
But I am getting below exception (randomly) with my below code of KVO registration and de-registration.
Is there any safe check that can be applied here.
*** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <MyPagingController 0x1f05e460> for the key path "contentOffset" from <UIScrollView 0x1f0a8fd0> because it is not registered as an observer.'
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.scrollView addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)viewWillDisappear:(BOOL)iAnimated {
[super viewWillDisappear:iAnimated];
[self.scrollView removeObserver:self forKeyPath:#"contentOffset"];
}
Your unsubscribing code somehow gets hit more often than the subscribing code. Unfortunately KVO does not handle this nicely and the failure throws an exception rather than just doing nothing as you would expect. You either need to make sure it only gets hit once, or at least catch the exception like this:
#try
{
[self.scrollView removeObserver:self forKeyPath:#"contentOffset"];
}
#catch (NSException * __unused exception) {}
}
As much as I love KVO, this need to balance observing and removing is a real issue. You will also get crashes if you nil an object that was KVO observing without telling the sending class to de-register you and the class then tries to send an update to the receiver object. Oops! Bad Access!
As there is no official way to ping to see if you have added an observer (which is ridiculous given that a huge part of what goes on in OS is based on KVO), I just use a simple flag system to ensure that nothing gets added more than once. In the example below, I have a bool flag to track the observer status.
#interface RepTableViewController ()
#property (nonatomic, assign) BOOL KVOSet;
#end
#pragma mark - KVOObserving
- (void)_addKVOObserving
{
//1. If the BOOL flag shows we are already observing, do nothing.
if (self.KVOSet) return;
//2. Set the flag to YES BEFORE setting the observer as there's no guarantee it will happen immediately.
self.KVOSet = YES;
//3. Tell your class to add you up for the object / property you want to observe.
[[ELRepresentativeManager sharedManager]addObserver:self
forKeyPath:#"myRepresentative"
options:NSKeyValueObservingOptionNew
context:nil];
}
- (void)_removeKVOObserving
{
//1. Do nothing if we have not set an observer
if (!self.KVOSet) return;
//2. If we have an observer, set the BOOL flag to NO before removing
self.KVOSet = NO;
//3. Remove the observer
[[ELRepresentativeManager sharedManager] removeObserver:self
forKeyPath:#"myRepresentative" context:nil];
}
Yes, using flags to check state is frowned upon by the coding-police. But there really is no other way to be sure except to bean count.
Whatever you do, remember that some classes need to observe even after viewWillDisappear is called (but the view still exists somewhere in the hierarchy), so you can't just do the observe/remove from viewWillAppear/WillDisappear trick. You may need to use delegate callbacks to the object that created the view (and will destroy it) to ensure you never leave a KVO calling class hanging. Having said that, I'm no expert in this stuff, so I'm sure there are people who can do this with way more finesse than my patented bean-counting lol
I'm trying to make a little binding system between my UILabel and my object Data using KVO. If my UI change, my data have to change, and if my data change my UI should refresh to display the new value.
The biggest issue I have is that I need to cast a custom object to a void* (context) with __bridge_retained void* - or CFBridgingRetain() - but I don't know where I should call CFBridgingRelease(). If call it in observeValueForKeyPath method I get a bad access error (I guess because my Reference Count to the object pointed by context is 0)
// viewDidLoad
// binding my label text with a custom data object
[self bindObject:_myLabel withPath:#"text" toObject:_user path:#"name"];
-(void) bindObject:(id)uiObj withPath:(NSString *)uiPath toObject:(id)dataObj path:(NSString *)dataPath
{
// custom object storing the object I want to bind and his path
PLSObjectPath* op = [[PLSObjectPath alloc] init];
op.theObj = dataObj;
op.thePath = dataPath;
PLSObjectPath* ob = [[PLSObjectPath alloc] init];
ob.theObj = uiObj;
ob.thePath = uiPath;
/* possible leak because I don't know where to call CFBridgingRelease */
[uiObj addObserver:self forKeyPath:uiPath options:NSKeyValueObservingOptionNew context:(__bridge_retained void*)(op)];
[dataObj addObserver:self forKeyPath:dataPath options:NSKeyValueObservingOptionNew context:(__bridge_retained void*)(ob)];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
PLSObjectPath *obj = (__bridge PLSObjectPath*) context;
PLSObjectPath* pairObj = [[PLSObjectPath alloc] init];
pairObj.theObj = object;
pairObj.thePath = keyPath;
// avoid infinite loop
[obj.theObj removeObserver:self forKeyPath:obj.thePath];
[obj.theObj setValue:change[#"new"] forKeyPath:obj.thePath];
[obj.theObj addObserver:self forKeyPath:obj.thePath options:NSKeyValueObservingOptionNew context:(__bridge_retained void*)(pairObj)];
}
Traditionally users of this have used a static char * as the context parameter, so as to differentiate the different observeValueForKeyPath messages. That said, it should be possible to do something as you are attempting.
What I would suggest is to switch from a custom object to a Core Foundation one, where you can do you own memory management explicitly. Thus I'd suggest changing PLSObjectPath to CFDictionary. You can first create a NSDictionary, then "transfer" it to the CF domain with the appropriate cast, and pass that CFDictionary object for context (which is now a retained CF object). Recast it in observeValueForKeyPath to a CFDictionary, properly ARC cast it to a NSDictionary, use that, then it should get released if you've done the ARC correctly. This is all a well understood paradyme - moving objects in and out of ARC.
Another way you could do it is us a static NSMutableDictionary, and use the context pointer to go to a int value, which when converted to a NSNumber is the key to the dictionary. If all KVO occurs on the main thread, you don't need to protect the dictionary, but if not then you will need to put all access to the dictionary behind a serial dispatch queue that in forces serial access on one thread.
The memory leak is from [obj.theObj removeObserver:self forKeyPath:obj.thePath]. You are removing the observer but as the system doesn't treat the context as an object, it will not release it. Also, there is no way for you to get the context from the observed object itself.
At some point you will need to stop all observation to allow your view to be deallocated. This should happen from viewDid(Will)Disappear:. To be able to release the PLSObjectPath:s at that point, you will need to store them somewhere, possibly an NSMutableArray. If you will store these anyway for this purpose, you don't need to use __bridge_retained. Also, in this case your PLSObjectPath:s might/should contain a void* pointing to the other context. This way, you save the creation of PLSObject in observeValueForKeyPath:ofObject:change:context:.
Just a comment, you should start the KVO from viewWill(Did)Appear: and not from viewDidLoad. It gives you a better KVO start/stop management and also removes the unnecessary observation while your view is not on the screen.
I have two NSManagedObject subclasses: Parent and Child. Parent contains many Child(ren) in an OrderedSet. When state changes in a Child I want the parent to know about it.
If I was programming in another language I might use events, having the Parent listening for events from each of its children, however given that target-action is limited to view components, all Objective C offers me is use of a global NSNotificationCenter. I definitely don't like the idea of a Model tapping into global notifications (listening directly via events is Ok in my book), so it seems my only alternative is Delegation. However using delegation between two NSManagedObjects seems like a dangerous idea, given the difficulty in ensuring one party does not lose its reference to the other.
Does anyone have any suggestions as to how I should be handling this?
Another option is key-value observing. Set it up like this:
const static void *kParentObservingChildSomePropertyContext = &kParentObservingChildSomePropertyContext;
[child addObserver: child.parent forKeyPath: #"someProperty" options: /*see below*/ context: kParentObservingChildSomePropertyContext];
the weird constant definition exists because every observing context should be unique so that you don't tread on superclass or subclass observation contexts. To see what options you need to set, consult the manual and compare with your specific needs. Now whenever that property on your child object changes, your parent will receive:
-(void)observeValueForKeyPath: (NSString *)path ofObject: (id)object change: (NSDictionary *)change context: (void *)context;
Check that you have the correct context for your observation, then handle the change. If you get a different context here, forward the message to super.
When you're done observing the path, you remove the parent as an observer of the child:
[child removeObserver: child.parent forKeyPath: #"someProperty" context: kParentObservingChildSomePropertyContext];
Use the same context pointer you used in -addObserver:forKeyPath:options:context so that the correct instance of the observation is removed.
However using delegation between two NSManagedObjects seems like a dangerous idea, given the difficulty in ensuring one party does not lose its reference to the other.
Delegating, observing and watching for notifications from specific objects all suffer from this problem. You need to make sure that the lifetime of your interest in the notifications matches the lifetimes of the objects involved, otherwise you could easily "leak" observation info or - and this is worse - send notifications to stale object pointers. None of these solutions is immune to that, though in the case of the Delegate pattern you can use a zeroing weak reference to ensure that when the parent object disappears, the child will no longer try to delegate to it.
I pick a field to watch in the view controller that will service the request...
[self addObserver:self forKeyPath:#"clientProgress.dateLastUpdated" options:0 context:nil];
I make sure in that same view controller that I remove the observer on dealloc (or whatever passes for dealloc in ARC).
- (void)dealloc {
[self removeObserver:self forKeyPath:#"clientProgress.dateLastUpdated"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqual:#"clientProgress.dateLastUpdated"]) {
// If this key path has changed, the browser needs to update its display.
NSError *error = nil;
if (![managedObjectContext save:&error]) {
UIAlertView *dialog = [[UIAlertView alloc] initWithTitle:#"Save Error" message:#"Error saving inserted record, contact Tech Support" delegate:self cancelButtonTitle:#"Ok" otherButtonTitles:nil];
[dialog show];
[dialog release];
exit(-1); // Fail
}
if ( changeIsComingFromLibraryInsert ) {
}
[conditioningTableView reloadData];
}
// Essential to call super class implementation - NSArrayController relies heavily on KVO
//[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
Finally, when I want to call the observer, I merely update the date...
// force a save by setting the modified date and the preceeding ViewController with then save and reload from the calling view controller
// as there is an observer running watching this key value
clientProgress.dateLastUpdated = [NSDate date];