I would like to be notified about a new insertion in the NSMutableSet and thus this is what I am doing, but for some reason it is not calling observeValueForKeyPath method
Just for test:
-(void)observ{
[self addObserver:self forKeyPath:#"connections" options:NSKeyValueChangeInsertion context:NULL];
[connections addObject:#"connectionName"];
}
This is never called:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if( [keyPath isEqualToString:#"connections"] ) {
NSLog(#"added new object");
}
}
Is NSMutablSet KVC ?
NSMutableSet is indeed KVO/KVC compliant. However, in order to receive the notifications with the way you have this set up, you need to implement the KVC accessor methods for a set. Information can be found here. Essentially, you have to implement methods called:
-countOfConnections
-enumeratorOfConnections
-memberOfConnections:
-addConnectionsObject:
-removeConnectionsObject:
-intersectConnections:
You must use these methods to access and mutate your set in order to receive KVO notifications.
Finally, in your -observeValueForKeyPath method, you can use the value of the key kind in the change dictionary to determine what type of mutation occurred (addition, deletion, etc.). The values can be found here and are listed under "NSKeyValueChange". Hope this helps.
Related
I'm programming a feed reader on the iOS platform and, when parsing the json result, i create an array with objects that represent every feed. These object'll be successively push to a NSTableView or to a map (they havgeolocated information). Sometimes happen that the feed does not return the location information, but return the foursquare Id of the place, so I have to contact the foursquare server and get the information.
Wath's the best way to do this? (I so want to know when all the feed in the array has retrieved their information, so I can push all on a map and in the tableview)
Thanks
You could create an NSOperationQueue and add the operations to fetch the information from the server, then observe the "operations" key path of the queue. When operations.count == 0, then do your push to the map and tableview.
Edit:
You aren't polling, per se - you are observing the property and receiving notification of any changes.
Add yourself as an observer of your queue's operations property.
[self.runningQueue addObserver:self forKeyPath:#"operations" options:0 context:NULL];
Then implement the following:
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
if (object == self.runningQueue && [keyPath isEqualToString:#"operations"]) {
if ([self.runningQueue.operations count] == 0) {
// push to table view and map view
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object
change:change context:context];
}
}
See this thread.
I'm relatively new to Core Data and KVC, but I would like some pointers on registering listeners for changes in Core Data objects. Here's the situation:
I have one NSManagedObject called Patient and another called Medication. A Patient may have many Medications, and a Medication has a startOn and endOn dates.
I'd like to somehow listen for changes to the endOn property of all Medication objects. When a change occurs, I would like to schedule a local notification on the iOS device. I've worked with local notifications before, but don't know where to put the code for it in this context.
Do I create the scheduling code in the App Delegate and somehow register the App Delegate to listen for changes in Medication objects? Does this need to be attached to the NSManagedObjectContext?
How is this done? Pointers would be much appreciated!
Thanks!
With Key Value Observing, you need some instance to do the observing. Sometimes that can be the same object that calls -setEndOn: on Medication; sometimes it might have to be something else. Let's assume that your app has a MedicationManager class - of which one instance is created. And, further assume that MedicationManager has an instance method -createMedicationWithName:startOn:endOn: like this:
- (Medication*) createMedicationWithName:(NSString*)medName startOn:(NSDate*)startDate endOn:(NSDate*)endDate
{
// Create and configure a new instance of the Compound entity
Medication *newMedication = (Medication *)[NSEntityDescription insertNewObjectForEntityForName:#"Medication"
inManagedObjectContext:[self managedObjectContext]];
[newMedication setName:medName];
[newMedication setStartOn:startDate];
[newMedication setEndOn:endDate];
// Set up KVO
[newMedication addObserver:self
forKeyPath:#"endOn"
options:NSKeyValueObservingOptionNew
context:nil];
return newCompound;
}
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqualToString:#"endOn"])
{
// ... schedule local notification on the iOS device for (Medication*)object.
return;
}
}
Or something like that.
Note, when you delete a Medication, then you would want to removeObserver... Also, when launching your app, you would need to establish MedicationManager as observer for existing Medications. I think that this could be as simple as iterating over all Medications and calling addObserver for each. If you have many Medications, then you might want to do this in a more 'lazy' manner (i.e., in -awakeFromFetch).
You will have to register observers when objects are fetched from the store the first time as well as when they are created. Instead of having to loop through all of the entries when you first fetch (which is error prone especially on the iPhone when unmodified fetched objects can be faulted when not retained) just use the awakeFromFetch and awakeFromInsert messages.
Also, in the example code below you can also keep up with the Patient's aggregate information like the soonest startOn and the soonest endOn by making transient properties on the Patient which store this information. The following code observes changes to the endOn in the Medication and offers you the ability to update the Patient's aforementioned transient "soonest endOn" or "soonest startOn"
- (void)addMyObservers
{
registeredObservers_ = YES;
[self addObserver:self forKeyPath:#"endOn" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)awakeFromInsert
{
// called when you create this object
[super awakeFromInsert];
[self addMyObservers];
}
- (void)awakeFromFetch
{
// called when you fetch this old object from the store
[super awakeFromFetch];
[self addMyObservers];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualToString:#"endOn"])
{
id newValue = [change objectForKey:NSKeyValueChangeNewKey];
// someone changed endOn so do something with this "newValue"
// check to see if the Patient needs the transient property for the soonest medication updated
// update any local notification schedule
}
}
// this is only required if you want to update the Patient's transient property for the soonest endOn or
- (void)setPatient:(Patient *)patient
{
[self willChangeValueForKey:#"patient"];
[self setPrimitivePatient:patient];
[self didChangeValueForKey:#"patient"];
// check to see if the Patient needs the transient property for the soonest medication updated
}
I'm interested in the value change of a particular key which I keep in NSUserdefaults. However, what I have is not working for me. observeValueForKeyPath does not get triggered.
Update: I think I've discovered the issue. Rather than using a defined constant, if I use a string then it gets fired.
[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:kSomethingInteresting options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
NSLog(#"Defaults changed, %#.%#", object, keyPath);
if ((object == [NSUserDefaults standardUserDefaults]) && [keyPath isEqualToString:kSomethingInteresting]) {
NSLog(#"kSomethingInteresting changed in defaults");
}
}
Not ideal but if I precede the addOberver line with:
NSString* keyToObserve = kSomethingInteresting;
And use that in the addObserver line then that works. Seems a bit fiddly?
So I'm going to scrap the use of a defined constant in this instance and in all instances where I need to observe something in userdefaults. Shame, as I like using them for key names throughout.
I'm using following function to get my application notified after the operation in nsoperationqueue has finished, so that I can schedule the task that's dependent upon the result of the operation. I'm using:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if([keyPath isEqual:#"isFinished"] && _operation == object)
{
NSLog(#"Our Thread Finished!");
[_operation removeObserver:self forKeyPath:#"isFinished"];
[self performSelectorOnMainThread:#selector(showDialog) withObject:nil waitUntilDone:YES];
}
}
My question is since mostly the tasks assigned to these operations are parsing of data if I try to tap some other button or basically do something that results in action, I get the following exception:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<Settings: 0x21b970>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: isFinished
I perfectly understand that since I try doing other things on main thread, because of which the call to main thread:
[self performSelectorOnMainThread:#selector(showDialog) withObject:nil waitUntilDone:YES];
fails to get executed. But what's the solution to this problem as I want both allow user do any action after making a request and also perform the action scheduled after finishing the task assigned to the operation.
Is it really possible?
Thanx in advance.
If you can require Mac OS X 10.6 Snow Leopard, or (I think) iPhone OS 3.0, you can avoid KVO entirely for this. Just create another operation for the work you want to do on the main thread, add the operation it needs to follow as a dependency, and then put the main-thread operation on the main queue:
NSBlockOperration *mainThreadOp = [NSBlockOperation blockOperationWithBlock:^{
[self showDialog];
}];
[mainThreadOp addDependency:backgroundOp];
[[NSOperationQueue mainQueue] addOperation:mainThreadOp];
NSOperation supports dependencies between operations on different queues. Just be careful not to make operations on different queues mutually dependent, because that will result in deadlock.
The reason your are seeing this problem is that you are not using the context pointer.
When you add or remove an observer, pass a context pointer. In your observeValueForKeyPath:ofObject:change:context: , check the context point to make sure the observation being passed belongs to you. If not, call super. It's possible to use self as the context point, or you can take the address of a static string, etc.
Here is an example:
Adding the observer:
[sample addObserver:self forKeyPath:#"finished" options:[self observationOptions] context:(void *)self];
Handling the change notification:
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == (__bridge void *)self){
if ([keyPath isEqualToString:#"finished"]){
// Handle the change here.
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
Removing the observer:
[sample removeObserver:self forKeyPath:#"finished" context:(void *)self];
Because your code was not able to pass one of the notifications to super in observeValueForKeyPath:ofObject:change:context:, that notification went in but never came out. This is why you got that exception.
It unfortunate that many of the Key-Value Observing examples available on The IntarWebs do not do this correctly and/or pass NULL as the context pointer (even Apple's documentation does this).
I'm trying to wrap my head around NSNotification but can't seem to get it to work. Think I'm misunderstanding how to register for an notification.
I have a bool as a property in my connection manager class. At initialisation I authenticate with a few servers and check if I can access an external URL (App will mainly be used on company intranet and an external connection isn't always possible)
The BOOL property will be changed from YES to NO if it cannot access the connection and as this can be responded at any time I thought it would be best to register a notification for when it changes. The property is called externalConnectionAvailable
[ConnectionManager addObserver:self forKeyPath:#"externalConnectionAvailable" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
and have the method:
-(void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(#"observer called");
}
But this doesn't get called. Am I doing something completely wrong?
Thanks
In this statement:
[ConnectionManager addObserver:self forKeyPath:#"externalConnectionAvailable" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
Assuming that you are following the "Cocoa Way" and using the usual naming scheme for classes and object instances, you appear to be trying to add an observer for an entire class, as opposed to an object instance.
You should have something like
ConnectionManager *connectionManagerInstance = // initialize manager...
...
[connectionManagerInstance addObserver:self forKeyPath:#"externalConnectionAvailable" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
It was something very stupid. I was just changing the property by calling externalConnectionAvailable not self.externalConnectionAvailable