Mutually exclusive NSTableView selections

While developing Weather Vane, I ran into a problem trying to bind a selection from one of two different tables to a location ivar, which can only reflect the value of one selection at a time. In Weather Vane’s preferences, when a user does a search for a city, multiple results may be returned, some international and some US. International and US results are placed in separate tables. If the user selects a US city, the ivar is updated as expected. If the user then selects an international city, the ivar is again updated, but if the user then reselects the original US city, the ivar is not updated. The reason had to do with how and when the NSTableViewSelectionDidChangeNotification is sent.

When the initial selection was made in the US table, the notification is sent, the same occurs when a selection is made in the international table. But when the same selection is made again in the US table, the original selection is still valid even though the US table lost focus when the international selection was made, so reselecting it does not trigger an NSTableViewSelectionDidChangeNotification.

When the NSTableViewSelectionDidChangeNotification is sent, an NSTableView delegate method is invoked (if you’ve chosen to be notified of selection changes, or set some class in your project as the NSTableView’s delegate):

– (void)tableViewSelectionDidChange:(NSNotification *)aNotification

My initial attempt to resolve the selection problem tried to handle the problem in this method by deselecting everything in the ‘other’ table (the one losing focus), but what I found is that this led to additional notifications being sent. Every selection triggers a notification AND every deselection triggers a notification as well. One could easily end up with looping notifications this way, which is a problem.

I have a small project which illustrates the problem. You can download it here. (Please note: Although I called the project mutex_tables, one should not confuse this with locks of any sort). The name was determined from the mutually exclusive nature of the tables.

When the tables are initially populated, things look like this:
Initially populated tables

Once a selection is made, an NSTextField below the tables has its stringValue set (like my location ivar).
Initial selection made, NSTextField updated.

After making a selection in the second table, the NSTextField is also updated:
Second selection made, NSTextField updated.

Notice, the selection in the first table, although grayed out since the table has lost focus, is still highlighted.

Now, if we select the same item in the first table, nothing happens. Since technically, there is no change in the selection, the notification is not fired and the text field is not updated.
Original selection made, NSTextField NOT updated..

The solution I found is to use another delegate method,

-(BOOL)tableView:(NSTableView*)aTableView shouldSelectRow:(NSInteger)rowIndex

to handle the deselection in the ‘other’ (unfocused) table.

When called, a selection is about to made in one of the tables. What I do then is deselect everything in the ‘other’ table. Now, in tableViewSelectionDidChange, I can just “find” the new selection and update the textfield accordingly. tableViewSelectionDidChange will be called once on the initial selection, and twice thereafter, but never more than twice. Two notifications seem to be unavoidable.

Here’s how things look now after I implemented the new delegate method:

Initial unselected view:
Initially populated tables

After first selection is made:
Initial selection made, NSTextField updated.

After second selection in the other table is made:
Second selection made, NSTextField updated.

But now with our newly implemented delegate method in place, reselecting the original item in the first table, things work properly:
Reselect initial item in the first table, NSTextField updated.

Nothing in the unfocused table is highlighted and as selections are made between the tables, only the current selection in the current table will be highlighted.

Here is my implementation of tableView:shouldSelectRow:

-(BOOL)tableView:(NSTableView*)aTableView shouldSelectRow:(NSInteger)rowIndex{

// Deselecting generates an NSTableViewSelectionDidChangeNotification
// notification.
if (aTableView == self.secondTableView) {
[self.firstTableView deselectAll:self];
}
else {
[self.secondTableView deselectAll:self];
}
return YES;
}

And my tableViewSelectionDidChange
// This will be called once on the initial selection
// twice thereafter.
– (void)tableViewSelectionDidChange:(NSNotification *)aNotification{

NSDictionary * item = nil;
NSUInteger index;

// Make sure we have the right table and ignore no-selection
if([aNotification object] == self.firstTableView && [self.firstController selectionIndex] != NSNotFound){
index = [self.firstController selectionIndex];
item = [self.firstTableArray objectAtIndex:index];
}
else if([aNotification object] == self.secondTableView && [self.secondController selectionIndex] != NSNotFound){
index = [self.secondController selectionIndex];
item = [self.secondTableArray objectAtIndex:index];
}

// When two notifications are sent, item will be nil on first notification
if (item) {
[self.selectionField setStringValue:[item objectForKey:@”test”]];
}
}

You can download the updated project here.

By the way, don’t forget to check the “Avoid Empty Selection” box for the associated NSArrayController.

I hope people find this useful. If someone has a better way, please feel free to comment.

7 thoughts on “Mutually exclusive NSTableView selections

  1. I found WeatherVane when Meteorologist stopped working this week. I’m very impressed. Keep up the good work. One suggestion. Perhaps a checkbox in preferences to add it to login items. Again, thanks for the app.

  2. Great Article Thanks, I had the exact same problem. One extra thing I had to do was untick “Avoid Empty Selection” in my NSArrayControllers in interface builder.

  3. Super fantastic!!! I had a very similar problem and your example helped me perfectly. After a whole day of trying to work out a solution and trying many ideas (from googling), I can now take a break. Thank you everyone.

  4. Great! I’m glad you found it useful. The only alternative I had was to redesign the UI. I really didn’t want to do that, so I finally hammered this out.

Leave a Reply

Your email address will not be published. Required fields are marked *