views:

177

answers:

2

My UI is not updating when I expect it to.

The application displays "projects" using a view similar to iTunes -- a source list on the left lets you filter a list (NSTableView) on the right. My filters update properly when they are examining any simple field (like name, a string), but not for arrays (like tags).

I'm removing a tag from one of my objects (from an NSMutableArray field called "tags") and I expect it to disappear from the list because it no longer matches the predicate that is bound to my table's NSArrayController.

ProjectBrowser.mm:

self.filter = [NSPredicate predicateWithFormat:@"%@ IN %K", 
                                               selectedTag, 
                                               @"tags"];

Project.mm:

[self willChangeValueForKey:@"tags"];
[tags removeAllObjects];
[self didChangeValueForKey:@"tags"];

I've also tried this, but the result is the same:

[[self mutableArrayValueForKey:@"tags"] removeAllObjects];

Interface Builder setup:

  • a ProjectBrowser object is the XIB's File Owner
  • an NSArrayController (Project Controller) has its Content Array bound to "File's Owner".projects
  • Project Controller's filter predicate is bound to "File's Owner".filter
  • NSTableView's column is bound to "Project Controller".name
A: 

I'm quite surprised that your first code snippet even compiles. It also may not quite work as you expect because

self.property = foo;

is syntactic sugar for

[self setProperty: foo];

Anyway, your problem may be that you are not observing tags. I'm not sure that a predicate automatically observes the keys in its query string.

JeremyP
Thank you for pointing out the syntax error, it occurred during my attempt to extract the relevant code. I've corrected it above.
KingRufus
I'm not observing tags, but the predicate seems to observe simple fields when needed. This filter updates the list whenever the name changes: [.. predicateWithFormat:@"%K == %@", @"name", selectedName] So if I'm not propagating the change notification, despite my efforts above, how can I fix that?The KVO ability to observe using keypaths like "project.someObject.someField" leads me to believe that these changes should "bubble up" to an observer without requiring glue code at every link in the chain/path. Currently, arrays break any chain in the keypath observing functionality of my code.
KingRufus
Another guess: it may be that the predicate is optimising your change out because you are not changing the tags object itself. Try `[tags release] ; tags = [[NSMutableArray alloc] init];` instead of `[tags removeAllObjects]`
JeremyP
This results in the same behavior, unfortunately.
KingRufus
A: 

I found this in the docs (KVC Compliance - Dependent Values):

Important: Note that you cannot set up dependencies on to-many relationships. For example, suppose you have an Order object with a to-many relationship (orderItems) to a collection of OrderItem objects, and OrderItem objects have a price attribute. You might want the Order object have a totalPrice attribute that is dependent upon the prices of all the OrderItem objects in the relationship. You can not do this by implementing keyPathsForValuesAffectingValueForKey: and returning orderItems.price as the keypath for totalPrice. You must observe the price attribute of each of the OrderItem objects in the orderItems collection and respond to changes in their values by updating totalPrice yourself.

So you cannot rely on KVO dependencies or notifications when there is a to-many relationship in the keypath. This applies to my array of tags, so I've added some code to patch this broken link.

When I add a project to the "projects" array:

[newProject addObserver:self forKeyPath:@"tags" options:NSKeyValueObservingOptionNew context:nil];  

And the important part:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([object isKindOfClass:[CProject class]] && [keyPath isEqualToString:@"tags"])
    {
        [self willChangeValueForKey:@"projects"];
        [self didChangeValueForKey:@"projects"];
    }
}

And to cleanup, when I remove a project:

[project removeObserver:self forKeyPath:@"tags"];

Not sure if this is the best solution, but it's keeping my list updated.

KingRufus