NSArrayController and KVO
Asked Answered
U

5

5

What do I need to do to update a tableView bound to an NSArrayController when a method is called that updates the underlying array? An example might clarify this.

When my application launches, it creates a SubwayTrain. When SubwayTrain is initialised, it creates a single SubwayCar. SubwayCar has a mutable array 'passengers'. When a Subway car is initialised, the passengers array is created, and a couple of People objects are put in (let's say a person with name "ticket collector" and another, named "homeless guy"). These guys are always on the SubwayCar so I create them at initialisation and add them to the passengers array.

During the life of the application people board the car. 'addPassenger' is called on the SubwayCar, with the person passed in as an argument.

I have an NSArrayController bound to subwayTrain.subwayCar.passengers, and at launch my ticket collector and homeless guy show up fine. But when I use [subwayCar addPassenger:], the tableView doesn't update. I have confirmed that the passenger is definitely added to the array, but nothing gets updated in the gui.

What am I likely to be doing wrong? My instinct is that it's KVO related - the array controller doesn't know to update when addPassenger is called (even though addPassenger calls [passengers addObject:]. What could I be getting wrong here - I can post code if it helps.

Thanks to anyone willing to help out.

UPDATE

So, it turns out I can get this to work by changing by addPassenger method from

[seatedPlayers addObject:person];

to

NSMutableSet *newSeatedPlayers = [NSMutableSet setWithSet:seatedPlayers];

[newSeatedPlayers addObject:sp];

[seatedPlayers release];

[self setSeatedPlayers:newSeatedPlayers];

I guess this is because I am using [self setSeatedPlayers]. Is this the right way to do it? It seems awfully cumbersome to copy the array, release the old one, and update the copy (as opposed to just adding to the existing array).

Uriiah answered 15/1, 2010 at 3:55 Comment(2)
How is the table view bound to the controller? Do you have each table column bound to a property of subwayTrain.subwayCar.passengers (e.g. column Name bound to subwayTrain.subwayCar.passengers.name)?Jd
yep exactly. And the passenger names are showing up when it launches.Uriiah
U
1

So, it turns out I can get this to work by changing by addPassenger method from

[seatedPlayers addObject:person];

to

NSMutableSet *newSeatedPlayers = [NSMutableSet setWithSet:seatedPlayers];
[newSeatedPlayers addObject:sp];
[seatedPlayers release];
[self setSeatedPlayers:newSeatedPlayers];

I guess this is because I am using [self setSeatedPlayers]. Is this the right way to do it?

First off, it's setSeatedPlayers:, with the colon. That's vitally important in Objective-C.

Using your own setters is the correct way to do it, but you're using the incorrect correct way. It works, but you're still writing more code than you need to.

What you should do is implement set accessors, such as addSeatedPlayersObject:. Then, send yourself that message. This makes adding people a short one-liner:

[self addSeatedPlayersObject:person];

And as long as you follow the KVC-compliant accessor formats, you will get KVO notifications for free, just as you do with setSeatedPlayers:.

The advantages of this over setSeatedPlayers: are:

  • Your code to mutate the set will be shorter.
  • Because it's shorter, it will be cleaner.
  • Using specific set-mutation accessors provides the possibility of specific set-mutation KVO notifications, instead of general the-whole-dang-set-changed notifications.

I also prefer this solution over mutableSetValueForKey:, both for brevity and because it's so easy to misspell the key in that string literal. (Uli Kusterer has a macro to cause a warning when that happens, which is useful when you really do need to talk to KVC or KVO itself.)

Unwearied answered 17/1, 2010 at 2:42 Comment(0)
T
7

I don't know if its considered a bug, but addObject: (and removeObject:atIndex:) don't generate KVO notifications, which is why the array controller/table view isn't getting updated. To be KVO-compliant, use mutableArrayValueForKey:

Example:

[[self mutableArrayValueForKey:@"seatedPlayers"] addObject:person];

You'll also want to implement insertObject:inSeatedPlayersAtIndex: since the default KVO methods are really slow (they create a whole new array, add the object to that array, and set the original array to the new array -- very inefficient)

- (void)insertObject:(id)object inSeatedPlayerAtIndex:(int)index
{
   [seatedPlayers insertObject:object atIndex:index];
}

Note that this method will also be called when the array controller adds objects, so its also a nice hook for thinks like registering an undo operation, etc.

Tepic answered 15/1, 2010 at 7:41 Comment(0)
P
2

I haven't tried this, so I cannot say it works, but wouldn't you get KVO notifications by calling

insertObject:atArrangedObjectIndex:

on the ArrayController?

Pitfall answered 15/1, 2010 at 20:12 Comment(1)
Yes, using the NSArrayController to mutate the array makes it so that the NSArrayController knows about the mutation, and it (and things bound to it) should update accordingly.Tay
U
1

So, it turns out I can get this to work by changing by addPassenger method from

[seatedPlayers addObject:person];

to

NSMutableSet *newSeatedPlayers = [NSMutableSet setWithSet:seatedPlayers];
[newSeatedPlayers addObject:sp];
[seatedPlayers release];
[self setSeatedPlayers:newSeatedPlayers];

I guess this is because I am using [self setSeatedPlayers]. Is this the right way to do it?

First off, it's setSeatedPlayers:, with the colon. That's vitally important in Objective-C.

Using your own setters is the correct way to do it, but you're using the incorrect correct way. It works, but you're still writing more code than you need to.

What you should do is implement set accessors, such as addSeatedPlayersObject:. Then, send yourself that message. This makes adding people a short one-liner:

[self addSeatedPlayersObject:person];

And as long as you follow the KVC-compliant accessor formats, you will get KVO notifications for free, just as you do with setSeatedPlayers:.

The advantages of this over setSeatedPlayers: are:

  • Your code to mutate the set will be shorter.
  • Because it's shorter, it will be cleaner.
  • Using specific set-mutation accessors provides the possibility of specific set-mutation KVO notifications, instead of general the-whole-dang-set-changed notifications.

I also prefer this solution over mutableSetValueForKey:, both for brevity and because it's so easy to misspell the key in that string literal. (Uli Kusterer has a macro to cause a warning when that happens, which is useful when you really do need to talk to KVC or KVO itself.)

Unwearied answered 17/1, 2010 at 2:42 Comment(0)
P
1

The key to the magic of Key Value Observing is in Key Value Compliance. You initially were using a method name addObject: which is only associated with the "unordered accessor pattern" and your property was an indexed property (NSMutableArray). When you changed your property to an unordered property (NSMutableSet) it worked. Consider NSArray or NSMutableArray to be indexed properties and NSSet or NSMutableSet to be unordered properties. You really have to read this section carefully to know what is required to make the magic happen... Key-Value-Compliance. There are some 'Required' methods for the different categories even if you don't plan to use them.

Phillida answered 23/12, 2012 at 21:27 Comment(0)
P
0
  1. Use willChangeValueForKey: and didChangeValueForKey: wrapped around a change of a member when the change does not appear to cause a KVO notification. This comes in handy when you are directly changing an instance variable.

  2. Use willChangeValueForKey:withSetMutation:usingObjects: and didChangeValueForKey:withSetMutation:usingObjects: wrapped around a change of contents of a collection when the change does not appear to cause a KVO notification.

  3. Use [seatedPlayers setByAddingObject:sp] to make things shorter and to avoid needlessly allocating mutable set.

Overall, I'd do either this:

[self willChangeValueForKey:@"seatedPlayers"
            withSetMutation:NSKeyValueUnionSetMutation 
               usingObjects:sp];
[seatedPlayers addObject:sp];
[self didChangeValueForKey:@"seatedPlayers" 
           withSetMutation:NSKeyValueUnionSetMutation 
              usingObjects:sp];

or this:

[self setSeatedPlayers:[seatedPlayers setByAddingObject:sp]];

with the latter alternative causing an automatic invocation of the functions listed under 1. First alternative should be better performing.

Principe answered 14/2, 2012 at 17:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.