Ok, so vote it down if you have to but I'm about ready to through something as this is making me crazy. I have a
core data application (OS X), with a couple
NSTableViews (Cell Based) connected via
NSArrayControllers. I have the Entity class' setup with some custom methods. I can add, remove, edit and do all kinds of stuff with the data - all is working great.
I decided to add a new column for a running sum and use the
@sum as I have seen used. No matter what I do, I keep getting the error.
I have an Entity "Store" and another Entity "Item", they have a to-many relationship. In the Item entity I have a name and price attribute.
On the main window, I have two
NSTableView's controller by
NSArrayControllers, one for the Store and one for the Item. The content of the Item
NSArrayController is controlled by the Store Controller - the selected item.
I added a new column to the Item
NSTableView, bound it to the Item Controller and set it's model key path to @sum.price - this causes an error.
I am probably missing something simple, any thoughts on how to do this correctly?
NSArrayController: - Object Controller - Entity Name: Store
- Bound to Main Controller's
- Similar, Model Key Path: price
The new sum column:
Bound to Item Array Controller
Controller Key: arrangedObjects
Model Key Path: @sum.price
The error I receive is: "the entity Item is not key value coding-compliant for the key "@sum"."
Best How To :
Consider your first column. It is bound to Item Controller,
name. Does each cell get an array of names? No. Each gets a single name.
Although that column binding is sometimes expressed as a key path like
Item Controller.arrangedObjects.name, the way it actually works is that the column as a whole shows the
arrangedObjects, one element per row, but
name is applied to each element of that set separately. So, each cell has a single name.
Now consider your new column. The rows again correspond to the
arrangedObjects of the Item Controller, but the model key path is applied to each element individually. But the model key path contains a collection operator,
@sum, which isn't appropriate for an individual element (
Item entity). Hence the error.
You could create a text field (outside of the table) which shows the sum of the price of all of the items of the selected store. You would bind the text field's Value binding to Item Controller,
@sum.price. The text field works differently than the table column since it has a single thing to display. It really does use the result of
[ItemController valueForKeyPath:@"[email protected]"]. The collection operator will be applied to a collection.
You could also bind a text field to Item Controller,
@sum.price to have it show the sum of the prices of the items selected in the item table.
Bindings don't provide any way to get a running sum, if I understand what you mean by that (first row shows the price of the first item, second row shows the sum of the prices of the first and second items, etc.). Such a running sum would be context dependent. A given row's value would depend on the previous rows' values. For example, sorting the table differently would mean that the running sum next to a given item would change, because the set of items before it had changed. Bindings can't do that. They don't know about position, index, or siblings.
To get a running sum, you'll need to not use bindings for the column. Make your view or window controller adopt
NSTableViewDataSource if it doesn't already. Then connect the
dataSource outlet of the table view to it.
In your data source class, implement
-tableView:objectValueForTableColumn:row:. Check the column
identifier. For any columns other than the running sum column, return
nil so it uses the value from the column bindings.
For the running sum column, the straightforward-but-inefficient implementation would be something like:
NSRange range = NSMakeRange(0, rowIndex + 1);
NSArray* rowsToSum = [self.itemController.arrangedObjects subarrayWithRange:range];
return [rowsToSum valueForKeyPath:@"@sum.price"];
You would also need a way to inform the table view when cells in the running sum column need to be reloaded (recomputed). You would use Key-Value Observing to observe
self for a change in the key path
@"itemController.arrangedObjects.price". You would set this up in
-windowDidLoad. Don't forget to tear it down when the controller is done.
When the change notification is delivered — i.e. when
-observeValueForKeyPath:ofObject:change:context: is called — you would call
-reloadDataForRowIndexes:columnIndexes: on the table view to indicate that all row indexes in the running sum column should be reloaded.
That should work but it will be horribly inefficient once you get a significant number of rows.
So, to optimize, you should cache the running sums, but you need to be careful to invalidate the cache appropriately.
Basically, have an instance variable like
_cacheIsValid. Like all instance variables, it will start out zero (false) by default. In
-tableView:objectValueForTableColumn:row:, you'd check if it's valid. If it's not, you would build it and record that it's valid. Then, or if it was already valid, just return the element for the requested row.
To build the cache, iterate over
self.itemController.arrangedObjects computing the running sum as you go and adding each value onto the end of an array. You could use a C-style array of primitive types or an
NSNumbers, as you prefer. (The memory management for C-style arrays can be made simpler by using an
NSMutableData for the buffer.)
You would invalidate the cache in
-observeValueForKeyPath:..., before telling the table view to reload the running sum column.
For the next step in efficiency, you might recompute the cache at that time and compare the values to the existing cache (if it's valid) as you go. Accumulate in an
NSMutableIndexSet the row indexes of only those rows for which the cached running sum actually changed and use that in the call to
-reloadDataForRowIndexes:columnIndexes:. That way, the table view only reloads the cells that actually changed.