How should I remove all items from an NSTableView controlled by NSArrayController? - macos

I'm using an NSArrayController, NSMutableArray and NSTableView to show a list of my own custom objects (although this question probably applies if you're just showing a list of vanilla NSString objects too).
At various points in time, I need to clear out my array and refresh the data from my data source. However, just calling removeAllObjects on my NSMutableArray object does not trigger the KVO updates, so the list on screen remains unchanged.
NSArrayController has no removeAllObjects method available, which seems really weird. (It does have addObject, which I use to add the objects, ensuring the KVO is triggered and the UI is updated.)
The cleanest way I've managed to cause this happen correctly is:
[self willChangeValueForKey:#"myArray"];
[myArray removeAllObjects];
[self didChangeValueForKey:#"myArray"];
...so I'm kind of having to do the KVO notification manually myself (this is in my test app class, that contains the myArray property, which is NSMutableArray, as mentioned.)
This seems wrong - is there a better way? From my googling it seems a few people are confused by the lack of removeAllObjects in NSArrayController, but haven't seen any better solutions.
I have seen this solution:
[self removeObjectsAtArrangedObjectIndexes:
[NSIndexSet indexSetWithIndexesInRange:
NSMakeRange(0, [[self arrangedObjects] count])]];
but this looks even more unpleasant to me. At least my solution is at least marginally self-documenting.
Did Apple not notice that sometimes people might want to empty a list control being managed via an NSArrayController object? This seems kind of obvious, so I think I must be missing something...
Aside: of course, if I add new items to the array (via NSArrayController), then this triggers a KVO update with the NSArrayController/NSTableView, but:
Sometimes I don't put any items in the list, because there are none. So you just see the old items.
This is a bit yucky anyway.

You don't remove items from a table view. It doesn't have any items—it just displays another object's items.
If you bound the array controller's content array binding to an array property of some other object, then you should be working with that property of that object. Use [[object mutableArrayValueForKey:#"property"] removeAllObjects].
If, on the other hand, you haven't bound the array controller's content array binding, then you need to interact with its content directly. Use [[arrayController mutableArrayValueForKey:#"content"] removeAllObjects]. (You could also work with arrangedObjects instead of content. If one doesn't work, try the other—I've only ever done things the first way, binding the array controller to something else.)

Had this problem as well and solved it this way:
NSArrayController* persons = /* your array controller */;
[[persons content] removeAllObjects];

Swift
#IBOutlet var acLogs: NSArrayController!
acLogs.removeObjects(acLogs.content as! [AnyObject])
worked for me.

Solution in Swift:
if let ac = arrayController
{
let range:NSRange = NSMakeRange(0, ac.arrangedObjects.count);
let indexSet:NSIndexSet = NSIndexSet(indexesInRange: range);
ac.removeObjectsAtArrangedObjectIndexes(indexSet);
}

Just an update that works in Swift 4:
let range = 0 ..< (self.arrayController.arrangedObjects as AnyObject).count
self.arrayController.remove(atArrangedObjectIndexes: IndexSet(integersIn: range))

Related

NSTableBackgroundView

I have a ViewController which has a single TableView within its content.
Within the ViewController I access the actual Table via:
NSView * myView = self.view.subviews[0];
myView = myView.subviews[0];
NSTableView *myTable = myView.subviews[0];
[self.targets sortUsingDescriptors: [myTable sortDescriptors]];
[myTable reloadData];
This actually is clumsy but seems to work. Except that when I mouse down within the table when the vertical scroll bars are active. Then, I raise an exception because in the final line of accessing subviews to reach myTable, the subview array has several NSTableBackgroundViews before the actual TableView that I created and want to access.
NSTableBackgroundView apparently is not documented and does not have a sortDescriptors method (hence the exception).
Perhaps someone can point out a better way to get to the table which is more reliable, or can someone say whether enumerating the final subview array to find the desired ClassOf NSTableView is safe?
Many Thanks!
in lieu of any other answers, I implemented planB, I took the array of subviews and enumerated on them to find the actual NSTableView. This seems to work when I bang on the UI to test it and no more flags are raised.
I wish there were a cleaner way I could discover to get the view I want without traversing all the subviews.

Is there an 'indirect' Cocoa function like in Excel?

i'm building an app in Xcode where there are a 81 textviews in the NIB, each with a sequential name, so box1, box2, box3, box4 etc.
When doing data manipulation i want to be able to use the data in each box to add to an array for example. What i would like to be able to do is put this in a loop, so for example something like:
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i=1; i<82; i++) {
[array addObject: [Indirect("box" & i).text];
}
Similarly when outputting back to the textviews, i want to be able to loop from the array rather than referring to each textview independently. so something like:
for (int i=1; i<82; i++) {
indirect("box" & i).text = [array objectAtIndex:i];
}
Any ideas? Sorry if this is obvious - fairly new to the game.
Consider the MVC design pattern. Your calculation shouldn't be based directly off the views (the UI) but rather off some state in the controller, which is set by the views. Each time a field is edited, it notifies your controller via target/action or via Cocoa Bindings. When that happens, the controller updates your data model (in your case, that means it updates the computation and probably reflects the result in another part of the UI - the "total" field).
In Cocoa, there are two ways to do it:
Add all of the fields to an array in awakeFromNib. Enjoy writing 82 addObject: messages.
Remove the fields from the nib and create them in a loop in code, adding each one to an array. (This is what I'd do.)
Once they're in an array, you can refer to them by index, same as you do with the strings.
But you mention that you're accessing the fields' text property. This only exists in Cocoa Touch, not in Cocoa. If you're using Cocoa Touch, then you have a third option:
Replace your 82 outlets with an outlet collection.
The value of an outlet collection property is an array, so you get to create your fields in the nib but still refer to them by index into the array in the code.
On the other hand, I'd probably still create them in code, even though I'm more pro-nib than most Cocoa Touch devs. Part of it is habit (I'm still almost entirely a Mac developer), but part of it is the DRY principle. If I create the fields in a loop in code, I can describe all of the fields exactly once, along with the ways in which they differ. I won't have the risk of changing one field and forgetting (or even just having) to update the others, or of going to change all the fields (again) and forgetting to change one.
I would handle this using the tags: you can set them from 1 to 81 in the nib (look for the field under Control).
Then in -awakeFromNib you can call [self viewWithTag:i] inside a for loop.
It's definitely less work than individual outlets, and I think even simpler than an outlet collection – filling in the number means you don't have to connect outlets for all the text fields.

Manual Cocoa Binding not changing Observed KeyPath

I'm changing a cocoa binding programatically. I'm binding a NSTextField's value to the selection of an ArrayController. After I manually change the binding, I'm getting the "not key-value coding compliant for the key.." error, with the key being the old key, not the new one.
Check out the code:
NSTextField *textField = [self listTextField];
NSDictionary *currentBindInfo = [textFieldTableViewCell infoForBinding:NSValueBinding];
NSLog(#"pre-change bindings for textField: %#", currentBindInfo);
/* Change the binding. [Tried unbind: first, no difference] */
[textField bind:NSValueBinding
toObject:[currentBindInfo valueForKey:NSObservedObjectKey]
withKeyPath:#"objectValue.iLifeProductName"
options:[currentBindInfo valueForKey:NSOptionsKey]];
/* Log the info so we can confirm it changed. debugging. */
NSLog(#"post-change bindings for textField: %#", [textFieldTableViewCell infoForBinding:NSValueBinding]);
To troubleshoot, I call 'infoForBinding' before and after the change and it looks to be changed correctly. I can see the old value, then I call bind:toObject... and dump the infoForBinding a second time, and the value has changed for the binding:
2011-07-06 22:36:23.137 My App 2011[14640:407] pre-change bindings for listTextFieldTableViewCell: {
NSObservedKeyPath = "selection.osxProductName";
NSObservedObject = "...sameTextField... 0x4009cc380>";
NSOptions = {...same... };
}
2011-07-06 22:36:23.138 My App 2011[14640:407] post-change bindings for listTextFieldTableViewCell: {
NSObservedKeyPath = "selection.iLifeProductName";
NSObservedObject = "...sameTextField... 0x4009cc380>";
NSOptions = {...same... };
}
But the code is still calling the original key:
2011-07-06 22:36:23.231 My App 2011[14640:407] [ valueForUndefinedKey:]: the entity ILifeVersion is not key value coding-compliant for the key "osxProductName".
--
The NSArrayController is bound to a ManagedObjectContext, the entity name is being changed earlier with this:
[[self listAC] setEntityName:entityName];
Is the original keyValuePath being cached somewhere that I need to clear out? Is there a message like willChange/didChangeValueForKeyValuePath that I need to send to the binding or arrayController when I change the observed keypath?
Ideas?
Thanks!
As #noa pointed out, you’re looking at the binding on the cell, but changing the binding on its control. That’s bound (ahem) to cause problems.
Replace this:
[textField bind:NSValueBinding
toObject:[currentBindInfo valueForKey:NSObservedObjectKey]
withKeyPath:#"objectValue.iLifeProductName"
options:[currentBindInfo valueForKey:NSOptionsKey]];
with this:
[textFieldTableViewCell bind:NSValueBinding
toObject:[currentBindInfo valueForKey:NSObservedObjectKey]
withKeyPath:#"objectValue.iLifeProductName"
options:[currentBindInfo valueForKey:NSOptionsKey]];
And see if it works better.
The explanation for this is a bit arcane, and I’m doing it from memory, so please excuse me if I get some of the details wrong.
Because NSControls and their NSCell works so closely together, you can actually bind to either the control or the cell in most instances, and you’ll get very similar results. That is, there’s code in the control to call the proper methods on its NSCell if the control’s been bound to, and vice-versa.
This means that if, in XIB, you bind to one or the other things will work, which is good. It also means you can bind to a cell in cases where you have multiple cells per view, so that’s good. HOWEVER, it can lead to confusion, because in fact you can actually bind to both your view and its cell, and in fact bind them in different ways, and then they’ll crosstalk.
In your example, I believe you’re adding a second binding to the NSControl in addition to the one on its NSCell. You’re doubly-bound. That’s no good.
In terms of best practice, I try to bind only to NSControls unless I have a good reason to drop down to NSCells. Partly because it matches what I do in XIB, partly because any standard helps reduce exactly this problem, and partly because NSCells are being gently deprecated.

NSDictionaryController doesn't seem to observe changes to content dictionary

I must be missing something simple, but I am having some trouble binding a tableView to an NSDictionaryController. Here is a model of my current scheme:
TableViewColumn --bindsTo-->DictionaryController.arrangedObjects.(value or key)
--bindsTo-->someClass.someClassMember.aDictionary.
I've tested the tableView by adding an entry to aDictionary on init, which is displayed correctly. But when another method produces an object that is then added to aDictionary, the TableView doesn't seem to update or even know that aDictionary now has two entries. I've tried everything I can think of. I am not directly accessing aDictionary....I've tried (in someClassMember) [self aDictionary setValue:forKey:], and [self setValue:forKeyPath:#"aDictionary"] and similar variations. The key is a string, so it should be KVC/KVO compliant, and I have '#synthesize'd aDictionary in someClassMember.
What am I missing? Why won't new entries to the dictionary show up in the tableView?
Thanks in advance
Try [self willChangeValueForKey:#"aDictionary"]; before adding the new item, and [self didChangeValueForKey:#"aDictionary"]; afterwards in someClass

Refresh Cocoa-Binding - NSArrayController - ComboBox

in my application I made a very simple binding. I have a NSMutableArray bound to a NSArrayController. The controller itself is bound to a ComboBox and it shows all the content of the NSMutableArray. Works fine.
The problem is : The content of the Array will change. If the user makes some adjustments to the app I delete all the items in the NSMuteableArray and fill it with new and different items.
But the binding of NSMutableArray <-> NSArrayController <-> NSComboBox does not refresh.
No matter if I remove all objects from the Array the ComboBox still shows the same items.
What is wrong here? Is my approach wrong or do I only need to tell the binding to refresh itself? I did not find out how to do that.
You're likely "editing the array behind the controller's back", which subverts the KVO mechanism.
You said:
I have a NSMutableArray bound to a NSArrayController.
How? Where does the array live? In a document, accessible via a KVC/KVO compliant -myArray / -setMyArray: set of accessors?
I'll bet you're directly telling the "myArray" ivar to -removeAllObjects, right? How will these KVC/KVO accessors "know" the array has changed?
The answer is, they don't. If you're really replacing the whole array, you'll want to tell your document (or whoever owns the array) to -setMyArray: to a whole new array. This will trigger the proper KVO calls.
... but then, you don't really need a mutable array, do you? If you only want to replace individual items in the array, you'll want to use indexed accessors:
(Documentation - see the Collection Accessor Patterns for To-Many Properties section)
http://tinyurl.com/yb2zkr5
Try this (using ARC/OS X 10.7):
in header file, define the arrayInstance and the arrayController
#property (weak) IBOutlet NSArrayController *arrayController;
#property (strong) NSArray *arrayInstance; // for the array instance
then in implementation
#synthesize arrayController = _arrayController;
#synthesize arrayInstance = _arrayInstance;
_arrayInstance = ....... // What ever the new array will be
[_arrayController setContent:_arrayInstance];
This will force the arrayController to update the content and display correctly.
Another but 2 line of code solution would be:
[self willChangeValueForKey:#"arrayInstance"];
_arrayInstance = ....... // What ever the new array will be
[self didChangeValueForKey:#"arrayInstance"];
Think the first looks more obvious, the second more KVO-like.
KVC/KVO compliance seems to be the problem. You should create the new array and update the reference with the new object by using the generated accessor methods. You may otherwise fire KVO messages about the array being updated to inform the bindings, that the contents of the array have changed.
Christian

Resources