How to get notified of changes to models via an NSArrayController? - objective-c

I have an NSView subclass which is bound to the arrangedObjects of an NSArrayController. When the array has an item inserted or removed the view is notified. How do I get it to be notified if a model stored in the array has an attribute changed?
Do I need to add my view as an observer to every (relevant) attribute of every item added to the array?
When an item is added to or removed from the array I am notified via observeValueForKeyPath:ofObject:change:context: in my NSView subclass. I am not notified of changes to the models stored in the array but I could, every time I am notified of an insertion, add the view as an observer to the new item's attributes. Is this the best way to do this?
I overrode addObserver for the model class so that I could see what happens and noticed that NSTableView columns bound to the arrangedObjects add themselves as observers to the appropriate attributes. Can this be made to happen automagically or do I set up the observations manually?

A big thank you to dreamlax but I think I didn't do a good enough job explaining my problem. My model class was observable and produced the right notifications but I couldn't work out how to observe them without observing every item in the array directly.
I think the documentation for key paths could be improved because I couldn't find anything that explained the very simple change I needed to make. There's some good info the array magic keypaths but no simple "these are the common things" documentation.
Anyway. Previously in my NSView subclass I had the following:
- (void) bind:(NSString *)binding toObject:(id)observable withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
{
if ([binding isEqualToString:#"observedObjects"]) {
[observable addObserver:self forKeyPath:#"arrangedObjects" options:0 context:nil];
} else {
[super bind:binding toObject:observable withKeyPath:keyPath options:options];
}
}
To get notification of changes to the models within the NSArrayController's arrangedObjects all I needed to add was observation of arrangedObjects.name (for the name property of my model). So the above code became:
- (void) bind:(NSString *)binding toObject:(id)observable withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
{
if ([binding isEqualToString:#"observedObjects"]) {
[observable addObserver:self forKeyPath:#"arrangedObjects" options:0 context:nil];
[observable addObserver:self forKeyPath:#"arrangedObjects.name" options:0 context:nil];
} else {
[super bind:binding toObject:observable withKeyPath:keyPath options:options];
}
}
That's it! Now if any object in arrangedObjects gets its name changed I am notified.

Maybe rather than observing potentially many key value paths, why not have each object in the array post a notification when something has changed, then only one object needs to observe one notification instead of one object observing many key value paths.
EDIT:
Also, your arrayed objects could also respond to a class method called +keyPathsForValuesAffecting<key> where <key> is your key name. Here's an example: paymentDue is a key, which is affected when the values invoiceItems.count or paymentsMade have changed. When invoiceItems.count or paymentsMade has changed, anything bound to paymentDue is sent a notification.
+ (NSSet *) keyPathsForValuesAffectingPaymentDue:
{
return [NSSet setWithObjects:#"invoiceItems.count", #"paymentMade", nil];
}
If you are running on 10.4, or are targeting 10.4 or earlier, you'll need to use this method instead, but it essentially boils down to the same thing.
EDIT 2:
To clarify your other comment; you can still have each object in the array manually call
[[NSNotificationCenter defaultCenter] postNotificationName:#"ModelDidChange" object:self];
Then, with some controller code you can register for notification updates from your objects. If you choose a unique notification name then you won't need to manually listen from specific objects, you can tell the NSNotificationCenter that you want to receive notifications from any object. Your controller can work out which object has changed quite easily.
Register with the notification center (these methods should be in a controller object):
// This string could really be just about anything you want, but make it conspicuous.
static NSString * const ModelDidChangeName = #"ModelDidChange";
- (void) awakeFromNib
{
// using nil for the object parameter will make the notification center
// invoke modelDidChange: regardless of who the sender is.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(modelDidChange:) name:ModelDidChangeName object:nil];
}
Implement a method to handle the notification.
- (void) modelDidChange:(NSNotification *) notification
{
MyModelClass *myObject = [notification object];
// do stuff with your model if necessary.
// do stuff with your view too
}
Get your model objects to post notifications when parts of them change:
#implementation MyModelClass
- (void) setSomething:(NSString *) newThing
{
[something autorelease];
something = [newThing copy];
if (something == nil)
{
// special case scenario for when something is nil
// do stuff, modify MyModelClass instance's attributes
[[NSNotificationCenter defaultCenter] postNotificationName:ModelDidChange object:self];
// the controller's modelDidChange: method is automatically invoked.
}
}
#end
But
If your model is properly KVC compliant and made with the appropriate KVO callbacks, manual notifications won't be necessary.

Related

How to get a list of updated attributes / relationships in managed object?

I have a notification handler. How can I get the list of updated attributes / relationships?
[[NSNotificationCenter defaultCenter] addObserver:[Utility class] selector:#selector(managedObjectContextDidSave:)
name:NSManagedObjectContextDidSaveNotification object:nil];
- (void)managedObjectContextDidSave:(NSNotification *)saveNotification {
NSArray *updated = [saveNotification.userInfo valueForKey:NSUpdatedObjectsKey];
for (NSManagedObject *obj in updated) {
//how to get changed attributes??
}
}
NSManagedObject has a changedValues property that contains the attributes and relationships that was changed and newly assigned values are in the dictionary too. And because managed object is available also for NSManagedObjectContextWillSaveNotification not only NSManagedObjectContextDidSaveNotification, so you get the chance to do additional modification on you managed object before saving without to use NSNotification object.

Pass index back to parent view controller

The NSMutableArray detailsDataSource and int detailIndex is passed on to next View Controller from
MainDetailViewController.m:
#import "UsersDetailViewController.h"
...
- (void)swipeDetectedUp:(UISwipeGestureRecognizer *)sender
{
UsersDetailViewController *usersController = [[self storyboard] instantiateViewControllerWithIdentifier:#"UsersController"];
[self.navigationController pushViewController:usersController animated:NO];
usersController.usersDataSource = [[NSMutableArray alloc] initWithArray:detailsDataSource];
usersController.userDetailIndex = detailIndex;
}
Swipe through the index in UserDetailViewController.m:
- (void)swipeDetectedRight:(UISwipeGestureRecognizer *)sender
{
if (userDetailIndex != 0)
userDetailIndex--;
}
When swipeDetectedDown to pop back, MainDataViewController needs to know which object at index to display:
- (void)swipeDetectedDown:(UISwipeGestureRecognizer *)sender
{
//jump to correct object at index, same as current object at index in this view
[self.navigationController popViewControllerAnimated:NO];
}
Code suggestions?
Use NSNotificationCenter to send an object back to the MainDataViewController...
Example:
In UsersDetailViewController populate an NSDictionary with a key=>value pair then send it over to where you want it to go.
NSArray *key = [NSArray arrayWithObject:#"myIndex"];
NSArray *object = [NSArray arrayWithObject:detailIndex];
NSDictionary *dictionary = [NSDictionary dictionaryWithObjects:object forKeys:key];
[[NSNotificationCenter defaultCenter] postNotificationName:#"MainDataViewController" object:self userInfo:dictionary];
Note: You need to setup an identifier on MainDataViewController called MainDataViewController or whatever you want to call it. Using the VC name keeps it simpler.
Then on MainDataViewController do this in the viewDidLoad() method.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(receiveNotification:) name:#"MainDataViewController" object:nil];
And then receive the notification by using the following method:
- (void)receiveNotification:(NSNotification *) notification
{
if([[notification name] isEqualToString:#"MainDataViewController"])
{
NSDictionary *dictionary = [NSDictionary dictionaryWithDictionary:[notification userInfo]];
if([dictionary valueForKey:#"myIndex"])
{
// do whatever you need to do with the passed object here. In your case grab the detailIndex and use it for something...
}
}
}
The easy part is to put the UsersDetailViewController pointer into a property of MainDetailViewController so it can access self.usersController.usersDataSource & self.usersController.userDetailIndex later. Then the only trick is to have it know when the UsersDetailViewController was popped.
In code I used to write, I often tried something like making MainDetailViewController be a delegate of UsersDetailViewController, and having a delegate method in MainDetailViewController be called when the UsersDetailViewController want to close programmatically, and in that do both the popViewControllerAnimated: and update the MainDetailViewController's state. In other words, always have the parent's code pop the child off. This works, but not in the case where you have the child view controller pop automatically via the navigation controller's back button say, so overall I'd argue against that technique.
I think there's better solutions for having the parent's code get called when its child is popped. Perhaps implement a viewWillAppear method and if self.usersController is set there, then you know you're coming back from the UsersDetailViewController, at that point access the other controller's properties and finally clear self.usersController.

Logging NSNotifications

I would like to log any NSNotifications posted by a single NSNotificationCenter shared accross my application. I have tried subclassing NSNotificationCenter with the intention of adding logging code to the three post methods, but it returns an instance of CFNotification center instead of my subclass.
Surely there is a way of monitoring NSNotification posting?
EDIT/UPDATE
As two answers below correctly point out I could listen to all notifications and log them in a handler, but the sequence the handler would receive these notifications is far from guaranteed to be the same as the sequence in which they were dispatched. If I could be sure the handler would always be the first hander to be notified this would work, but I cannot: 'The order in which observers receive notifications is undefined' From NSNotification Docs
By using - addObserver:selector:name:object: and passing nil for both the name and the object, you will get notified about any notification.
- (id)init
{
self = [super init];
if (self != nil)
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(log:) name:nil object:nil];
}
return self;
}
- (void)log:(NSNotification *)notification
{
NSLog(#"%#", notification);
}
Edit: if you want to get the real order of the notifications being send, try subclassing NSNotificationCenter and overriding the following methods:
– postNotification:
– postNotificationName:object:
– postNotificationName:object:userInfo:
If subclassing is no option for you, you might consider defining a category on NSNotificationCenter where you override these methods with calling the super implementation. (you will need to swizzle methods to call super within a category). Tell me if you need help doing so.
You should be able to use [addObserver:self selector:#selector(whatever:) name:nil object:nil] and just put your logging code in the whatever: method. This observer should get all notifications posted by your app (at least all those posted by the default center).

Why are my Cocoa bindings broken?

I have a window with an NSTextField (in Snow Leopard), which I have binded to an NSString function in my WindowController class. This string will combine information about my table view's selection and count, provided by my array controller. It gets an initial value, "0 0", but doesn't ever update, when the selection or count changes. The binding looks like this (File's Owner is MyWindowController):
I implemented + (NSSet *)keyPathsForValuesAffecting<key> (below), but the binding never updates, even when the array controller's total count and selection change.
(Additional troubleshooting performed) I had originally been using the Display Pattern Value binding of the NSTextField, but I needed more complicated logic than that binding afforded. I then started listening to the selection changed/changing events of the TableView that displays the array controller's contents and changing the Display Pattern Value bindings dynamically, but that felt like a hack, and overly complicated.
I'm sure there's something I'm missing, but I can't tell what. Does anyone have any ideas? I've read through Apple's key-value-observing documentation, and this seems to be all that's necessary. I've checked, and my keyPathsForValuesAffectingMyString is getting called, but myString only gets called once. I've distilled my code below (updated x3).
Update 1/21
I'm still plugging away trying to figure this out. When I addObserver to self for the arrayController key paths, the notifications do fire as expected, so my key paths and the key value observing mechanism is fine. When I call [self didChangeValueForKey:#"myString"]; within my observeValueForKeyPath method for the same keys, the binding still doesn't update, leading me to believe it's a bindings problem rather than a KVO problem. I'm going to be reading up on the bindings mechanism more...
#interface MyWindowController : NSWindowController {
IBOutlet NSArrayController *arrayController;
}
- (NSArrayController *)arrayController;
- (NSString *)myString;
#end
#implementation MyWindowController
+ (NSSet *)keyPathsForValuesAffectingMyString {
return [NSSet setWithObjects:
#"arrayController.arrangedObjects",
#"arrayController.selection",
nil];
}
- (NSArrayController *)arrayController {
return arrayController;
}
- (NSString *)myString {
// Just as an example; I have more complicated logic going on in my real code
return [NSString stringWithFormat:#"%#, %#",
[arrayController valueForKeyPath:#"arrangedObjects.#count"],
[arrayController valueForKeyPath:#"selection.#count"]];
}
#end
I’ve verified this exact same bug. Someone on Cocoabuilder had a guess as to why the bug happens:
http://www.cocoabuilder.com/archive/cocoa/284396-why-doesn-nsarraycontroller-selection-et-al-fire-keypathsforvaluesaffectingkey.html#284400
I can’t speak as to whether this explanation is true, but I certainly can’t get +keyPathsForValues… to work with NSArrayControllers.
I've got a workaround, but I'm not happy about it, since it shouldn't be necessary, and I would still prefer to get the bindings working properly. I won't accept this answer, and will delete it if someone posts an actual fix. </disclaimer>
#interface MyWindowController : NSWindowController {
IBOutlet NSArrayController *arrayController;
IBOutlet NSTextField *fieldThatShouldBeBinded;
}
- (NSString *)myString;
#end
#implementation MyWindowController
- (void)awakeFromNib {
[arrayController addObserver:self
forKeyPath:#"selection"
options:0
context:NULL];
[arrayController addObserver:self
forKeyPath:#"arrangedObjects"
options:0
context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if( object == arrayController )
[fieldThatShouldBeBinded setStringValue:[self myString]];
}
- (NSString *)myString {
return [NSString stringWithFormat:#"%#, %#",
[arrayController valueForKeyPath:#"arrangedObjects.#count"],
[arrayController valueForKeyPath:#"selection.#count"]];
}
#end
Make sure that the arrayController outlet is connected in Interface Builder. I'm guessing that it's nil.
Don't use the #count keyword. Bindings and KVO on array controllers will get updated when the content changes. If that doesn't work, then there is a problem somewhere else.
Another option is to use the display pattern bindings instead of a composite property. Bind Display Pattern Value1 to arrayController.arrangedObjects.#count and Display Pattern Value2 to arrayController.selection.#count, and set the pattern to "%{value1}#, %{value2}#"
I met the same problem and found another way (but it is still workaround).
You have to declare dynamic workaround property. In implementation section, just return new empty object for it. Now, you can KVO this workaround property.
#property(nonatomic,retain) NSArray *workaround;
#dynamic workaround;
- (NSArray *)workaround { return [NSArray array]; } // new *every* time
- (void)setWorkaround:(NSArray *)unused { }
+ (NSSet *)keyPathsForValuesAffectingMyString { return [NSSet setWithObject:#"workaround"]; }
To get this work, you still need to manually bind self.workaround to arrayController.selectedObjects (or whatever):
- (void)awakeFromNib // or similar place
{
[super awakeFromNib];
[self bind:#"workaround" toObject:arrayController withKeyPath:#"selectedObjects" options:nil];
}
Manual binding works as expected, workaround is updated with what you have bound it to. But KVO tests whether property value is really changed (and stops propagating if it is the same). If you return new self.workaround value every time, it works.
Warning: never call -[setWorkaround:] by yourself — this will effectively flush the other side of binding (arrayController.selectedObjects in this case).
This method has some benefits: you avoid centralized observeValueForKeyPath:... and your logic is in the right place. And it scales well, just add workaround2, 3, and so on for similar cases.

How do I tell a (managed) object to notify its KVOs that one of its properties needs to be recached?

When we have an object who has a property that's generated based on other properties, usually we implement the +keyPathsForValuesAffecting{PropertyName} class method.
What I'm trying to do is basically the same thing for a property on my NSManagedObject, but traversing a relationship.
My model is simple; I have two Entities, App and Version (I'm creating an appcast-generating app). When the App's properties are changed, because I implemented the method above, the -appcast string is changed, and all bindings are updated appropriately.
However, when any properties on any of a specific App's Versions (to-many relationship) change, the -appcast property is not generated appropriately. I can haz fix/workaround?
This is a bit of a late answer, but I think it's a common situation, and the answer definitely isn't readily-apparent.
I generally watch for the managedObjectContext to change and then check if the any of the changed objects are ones that I want to look out for. So, in your NSManagedObject subclass:
// We need to register for the notification in both awakeFromFetch
// AND awakeFromInsert, since either one could be called, depending on
// if the object was previously-created or not
- (void)awakeFromFetch {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(managedObjectContextDidChange:) name: NSManagedObjectContextObjectsDidChangeNotification object:[self managedObjectContext]];
}
- (void)awakeFromInsert {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(managedObjectContextDidChange:) name: NSManagedObjectContextObjectsDidChangeNotification object:[self managedObjectContext]];
}
- (void)managedObjectContextDidChange:(NSNotification *)notification {
// Get a set containing ALL objects which have been changed
NSSet *insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
NSSet *updatedObjects = [[notification userInfo] objectForKey:NSUpdatedObjectsKey];
NSSet *deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];
NSSet *changedObjects = [insertedObjects setByAddingObjectsFromSet:updatedObjects];
changedObjects = [changedObjects setByAddingObjectsFromSet:deletedObjects];
if ([changedObjects intersectsSet:[self versions]]) {
[self willChangeValueForKey:#"appCast"];
[self didChangeValueForKey:#"appCast"];
}
}
This is certainly not ideal from a performance perspective, since this notification is going to fire every time anything in your object graph changes, but I've found it to be the most straightforward way to accomplish this.