How to programmatically monitor KVC object? - objective-c

I'm trying to monitor a NSMutableArray for changes via code. I want to add an observer for whenever the array changes, but I don't see what the NotificationName is supposed to be to make that happen.
Basically, when the array is modified, I want to execute a custom selector.

I'm not 100%, but I'm pretty sure that Key-Value Observing is what you want.
Whatever object it is that cares about the array registers itself as an observer:
[objectWithArray addObserver:self
forKeyPath:#"theArray"
options:NSKeyValueObservingOptionNew
context:nil];
It will then receive notice that the array has changed:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
NSLog(#"Change is good: %#", [change objectForKey:NSKeyValueChangeNewKey]);
}
Note that this one method will collect all the observations that this object has registered for. If you register the same object to observe many different keys, you will likely have to differentiate them when this method gets called; that's the purpose of the keyPath and object arguments.
The problem, and the reason I'm not sure if this will work for you, is that this assumes that the array is in your code, because you need to wrap accesses to it in order for the notification to be sent.
[self willChangeValueForKey:#"theArray"];
[theArray addObject:...];
[self didChangeValueForKey:#"theArray"];
An arbitrary framework class will have some properties which are, and some properties which are not, Key-Value Observing compliant. For example, NSWindow's firstResponder is KVO compliant, but its childWindows is not. The docs, of course, will tell you which are which.

Related

How to set value without trigger KVO

I use the following code to add KVO on object.
[self.model addObserver:self
forKeyPath:#"userName"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
Now I want to set the userName like below. Yes, it will trigger KVO.
self.model.userName = #"testUser";
However, I want to set value without trigger KVO. How to do that? Are there any method like below allowing me to do that?
[self.model setValue:#"testUser" forKey:#"userName" isSilent:YES];
Your design is broken if you want to do this. The point of key-value observing is that someone wants to know when a field changes so they register for notifications. The point of being key-value observing compliant is that you're keeping your options open as to how the rest of the system interacts with you.
What it sounds like you're doing is trying to hack around some problem where you don't want someone to know the true value of a property for some reason. So they think they're getting updates but actually if they were to check the property then it'd turn out you were deliberately lying.
As you'd expect, Cocoa doesn't have any mechanisms to support such hacks. They're extremely bad practice, breaking the whole structure of object-oriented programming.
Lecturing aside, you could write a custom setter that went directly to the instance variable. So, e.g.
- (void)setUserNameAndLieAboutItAsAShortSightedHack:(NSString *)newName
{
_userName = newName;
}
At the system level, key-value observing is implemented by creating a new version of the property setter that contains a call to the real setter and makes appropriate observer calls around the outside. So avoiding the real setter would avoid the notifications.
Core Data implements setPrimitiveValue:forKey: to allow you to do this. You can implement the same method in your object.
[self.model setPrimitiveValue:#"testUser" forKey:#"userName"];
When doing this however, it should be in the context of aggregating notifications where the observer is eventually notified with manual willChangeValueForKey: and didChangeValueForKey:.
You can use an ignore flag. Same idea as in the docs for User-Driven Updates.
// update the config object with the value the user chose.
- (IBAction)valueChanged:(UISlider *)slider{
self.ignore = YES;
self.config.sliderValue = slider.value;
self.ignore = NO;
}
// update the slider with the value from the config object.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == kSliderKVOContext) {
if(!self.ignore){
self.slider.value = [change[NSKeyValueChangeNewKey] doubleValue];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}

Grand Central Dispatch (GCD) + Key-Value Observing (KVO)

I have a method in which I add observers:
- (void) method
{
[currentPlayer addObserver:self forKeyPath:#"some" options:some context:some];
}
All the changes are processed in these method:
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
If I modify my method to:
- (void) method
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[currentPlayer addObserver:self forKeyPath:#"some" options:some context:some];
});
}
Does it mean that - (void) observeValueForKeyPath... will work in DISPATCH_QUEUE_PRIORITY_HIGH?
P.S. I want KVO to work in DISPATCH_QUEUE_PRIORITY_HIGH.
EDIT:
I observe AVPlayer variables. Such as:
[currentPlayer addObserver:self forKeyPath:#"currentItem.loadedTimeRanges" options:NSKeyValueObservingOptionNew context:kTimeRangesKVO];
These variables are changed automatically.
Every time "observeValueForKeyPath" is called I check the queue and it remains dispatch_get_main_queue() and I don't know how to change it to some other queue.
As an observer you can't control what thread / queue observeValueForKeyPath is called from. You could wrap a dispatch onto the queue you wish around the work this method doing. That will move the work of the observation to the high priority queue.
Just be sure you're aware this work is likely to be running on a different thread / queue than the code that triggered the observation. If the object you're observing is not thread safe you will need to inspect it prior dispatching the rest of the work onto a different queue.
No, if you want that the observeValueForKeyPath method gets executed with a higher priority, you have to change the key value from a thread with higher priority.KVO works synchronously, so the observeValueForKeyPath method gets executed on the same thread where you change the key value.

How can I invoke a method as soon as a bool flag changes?

I want to perform [document doSomething] as soon as [document isBusy] is false. What is the best way to do it? I tried it with a while-loop but that delays the following code, which is not what I want.
If the action a semantic of your document model
From your question both isBusy and doSomething are features of your document and the action to doSomething when isBusy goes false is appears to be a semantic of your document model and should therefore be implemented by your document model, i.e. something like:
- (void) setIsBusy:(BOOL)flag
{
if(flag != _isBusy) // check if this is a change
{
_isBusy = flag;
if(flag)
{
// doSomething, or schedule doSomething if it is a long operation etc., e.g.
[self doSomething];
}
}
}
Using KVO to implement semantics within a single object is probably unusual, but there are cases where it is useful. In this case it would replace a direct action with an indirect one - KVO would execute doSomething at exactly the same point as the above sample code, there would just be a number of intermediate system methods between setIsBusy and doSomething plus the associated overhead of setting up the KVO.
If the action is a semantic of your document's client
Of course, if the linkage between these two is independent of your document model, i.e. is a semantic of the client of your document, then KVO is appropriate and would be implemented in your client. Your client would register as an observer of your document, i.e. something like:
[document addObserver:self
forKeyPath:#"isBusy"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:NULL];
in a method of the client. Then in your client when a notification of the change is received take the appropriate action:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqual:#"isBusy"] && object == document)
[document doSomething;
else
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
If isBusy has a associated setter method, you can call doSomething in that setter. A better option would be to use Key Value Observing to observe the value of isBusy and take action when it changes.
Use KVC/KVO (Key Value Coding and Key Value Observation). The subject is covered well in the Apple Documentation. KVO and KVC is a basic programming technique which provides may solutions to your problem.

How do I observe the "operations" property of NSOperationQueue?

I want to be notified whenever a NSOperation has been added or removed from my NSOperationQueue. I'm trying to set up key-value observing for the "operations" property (an array of NSOperations currently in the Queue) but its not being fired. Is there something wrong with my syntax?
#implementation myOperationQueueSubclass
-(id)init
{
if (self = [super init])
{
// Initialization code here
[self addObserver:self
forKeyPath:#"operations"
options:0
context:nil];
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
NSLog(#"queue changed...");
if ([keyPath isEqualToString:#"operations"]) {
if (self.operationCount == 0) {
// No ops
} else {
// Has ops
}
}
}
There's nothing wrong with your syntax, but you're observing the array property, not the array itself (which doesn't issue KVO notifications when it's mutated anyways).
You'll get notified if it's reassigned for some reason, but NSOperationQueue would have to take the trouble to make the notifications manually, or use the KVC accessors, to allow others to see when objects are added to or removed from its operations.
I had a similar need and created a very thin operations manager, OperationsRunner in this github project. Other objects interface with this class instead of the NSOperationsQueue directly. It has only a handful of methods - run an operation, cancel it, ask for the number of operations in the queue, etc.
What I did was to use a mutable set to hold a reference to an operation that was added to the operations queue, and remove it when the operation completed or cancelled - sort of a shadow container.
The nice this about this class is that you can easily add it to any kind of other class to manage operations, and quickly cancel all pending operations.

NSMutableDictionary KVO

I'm trying to observe changes in dictionary using KVO.
Example:
dictionary = [NSMutableDictionary new];
[dictionary setObject:#"test1" forKey:#"key1"];
[dictionary setObject:#"test2" forKey:#"key2"];
[dictionary setObject:#"test3" forKey:#"key1"];
I'd love to be able to hook an observer for whenever a value is added to the dictionary. removed, or replaced (ie in the above cases, whenever any of the setObject methods are called)
So in conclusion:
I want a function to have
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
called when I ADD a any new entry to a dictionary, or Remove any entry, or REPLACE any entry.
NOT: I do NOT want to have to specify which keys I'm observing for. (eg observe only when #"key1" is added) as this solution doesn't scale.
Subclassing NSMutableDictionary is a bit annoying, due to the fact that NSDictionary and its friends are class clusters. It's certainly doable, and if you have to pass the object itself to another set of classes, then you may want to do exactly that. Otherwise, it might be easier to create a composite class which has the same basic API and uses NSMutableDictionary object internally for storage. There's a pretty good write-up as CocoaWithLove.com, Ordered Dictionary Subclassing, which goes into doing this.
However, that doesn't completely solve your problem. What I would suggest is that you begin with a subclass or decorator class such as the one above, then add support explicitly for -(NSArray*)allKeys, which is a standard accessor in NSDictionary itself. Then, you can add support to pass along change messages for allKeys, which will make it observable.
This can be done by adding the following code around the -setObject:forKey: and -removeObjectForKey: methods.
- (void)setObject:(id)anObject forKey:(id)aKey
{
BOOL addKey=NO;
if (![dictionary objectForKey: aKey]) {
addKey=YES;
[self willChangeValueForKey: #"allKeys"];
}
[dictionary setObject:anObject forKey:aKey];
if (addKey)
[self didChangeValueForKey: #"allKeys"];
}
- (void)removeObjectForKey:(id)aKey
{
[self willChangeValueForKey: #"allKeys"];
[dictionary removeObjectForKey:aKey];
[self didChangeValueForKey: #"allKeys"];
}
What is being done here is that we're adding explicit KVO notification to the class when the dictionary's keys are changed to mark a change in the array.
This will take care of adds and removes. If you want changes to be notified on the same basis, you can remove the if statements, and just have allKeys notify on either set or remove, like this:
- (void)setObject:(id)anObject forKey:(id)aKey
{
[self willChangeValueForKey: #"allKeys"];
[dictionary setObject:anObject forKey:aKey];
[self didChangeValueForKey: #"allKeys"];
}
Then, in your code, you put in a single observer for the key #"allKeys" on this object and you'll be receiving notifications whenever an item changes.
I solved a similar problem by adding an observer to the mutable dictionary "translator" in this way:
[self addObserver:self forKeyPath:#"translator.#count" options:0 context:NULL];
My app manages the data in a the classical way, using a tableview, a controller and the dictionary as KVO property "translator".
The dictionary is bound to a NSDictionaryController in my XIB, and a tableview content is bound to the controller.
This are the connections of the tableview:
Now, in any of the following cases I catch the change :
adding a key-value pair
removing a key-value pair
changing a key
changing a value
Remark: unfortunately, this approach does not work with NSMutableArrays.
Changes are not recognized
Can't you subclass NSMutableDictionary and override the various setters? For instance, overriding setObject:forKey: by calling super, then immediately calling addObserver...
You can also write a wrapper for NSMutableDictionary where you force yourself to use custom setters to manipulate the underlying NSMutableDictionary.
Maybe I need more context to any of your limitations or scalability intents.
I hope this will be helpful
- (void)addObserver:(id)observer {
for (id key in grid)
[self addObserver:observer
forKeyPath:[key description]
options:0
context:key];
}
I think another way to do this is using the below override, incase you are observing NSMutableDictionary "allRecentCurrencyData" whose values are dependent on recentBrazilReals, recentEuEuro, recentUkPounds, recentJapanYen, the observer will get called, but the drawback is you need to know the keys before hand to do this.
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:#"allRecentCurrencyData"]) {
NSArray *affectingKeys = #[#"recentBrazilReals", #"recentEuEuro",#"recentUkPounds",#"recentJapanYen"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}