I have a NSArrayController and a NSTableView. They show tracks from iTunes. I can sort the list by clicking in the header.
Is there a way to set up a default sort descriptor for the table view so it sorts for albums every time the user launches the app?
I tried to set the sortDescriptor on the array controller and the table view but that changes nothing.
Thank you
Edit: The answer is right. But it needs a NSArray:
- (NSArray *)mainSortDescriptor {
return [NSArray arrayWithObjects:
[NSSortDescriptor sortDescriptorWithKey:#"album" ascending:YES],
[NSSortDescriptor sortDescriptorWithKey:#"trackNumber" ascending:YES],
nil];
}
If you want to bind the array controller's sort descriptor, you have to bind it to something. You can put this in your application delegate, for example:
- (NSArray *)tracksSortDescriptors {
return [NSArray arrayWithObject:
[NSSortDescriptor sortDescriptorWithKey:#"albumName"
ascending:YES]];
}
Then you can set up the binding in IB as
Bind to: MyAppDelegate
Model Key Path: tracksSortDescriptors
EDITED. I forgot, when translating this from PyObjC, that I was returning a list. Oops.
I tried this, didn't quite work - resorted on each app start, but not while the app was running.
Eventually, I noticed that in my NSArrayController object, the following box was unticked (argh!):
"Auto rearrange content"
...so, FYI to anyone who has the same problem: make sure that box is ON :)
Related
I'm fairly new to obj-c and cocoa so please bear with me:
I have a NSTableView set up with cocoa bindings which works as expected with the simple -add -remove, etc methods provided by an instance of NSArrayController in my nib. I would like to programmatically add objects to the array that provides content for this controller (and hence for the table view) and then update the view accordingly.
I current have a working method for adding a new object to the array (verified by NSLog) but I can't figure out how to update the table view.
So: How do I update the bound tableview? (ie, after I have programmatically added objects to my array). I'm essentially after some view refreshing code like [view reloadData] in glue code, but I want it to work with the bindings I have in place.
Or is there a KVC/KVO related solution to this problem?
Code Details:
AppController.h
#interface AppController : NSObject
#property NSMutableArray *clientsArray;
-(IBAction)addClientFooFooey:(id)sender;
#end
AppController.m (note, I also have the appropriate init method not shown here)
#implementation AppController
...
-(IBAction)addClientFooFooey:(id)sender{
[self.clientsArray addObject:[[Client alloc] initWithFirstName: #"Foo" andLastName:#"Fooey"]];
//Need some code to update NSTableView here
}
#end
Client.h just simply defines two properties: firstName and lastName. The 2 columns of an NSTableView in my mainmenu.nib file are appropriately bound to these properties via an array controller bound to my AppController instance.
On a side note/as an alternative. How could I add functionality to the existing NSArrayController method -add, ie, something like: -addWithFirstName:andLastName and still have this compatible with bindings?
You have two main options for doing this provided your array controller is bound to clientsArray.
The first way is to just use the array controller's addObject: method instead of adding objects directly to clientsArray.
The other way is to keep your current addClientFooFooey: method but wrap your existing code with these two lines:
[self willChangeValueForKey:#"clientsArray"];
[self didChangeValueForKey#"clientsArray"];
This tells the KVO system that you are making a change to the array so it will go and look at it again.
The first option is the most straightforward, but if for some reason you need to update the array directly just let KVO know you are doing it.
The Background
I've built a source list (similar to iTunes et al.) in my Cocoa app.
I've got an NSOutlineView, with Value
column bound to arrangedObjects.name
key path of an NSTreeController.
The NSTreeController accesses
JGSourceListNode entities in a Core
Data store.
I have three subclasses of
JGSourceListNode - JGProjectNode,
JGGroupNode and JGFolderNode.
I have selectedIndexPaths on NSTreeController bound to an NSArray called selectedIndexPaths in my App Delegate.
On startup, I search for group nodes and if they're not found in the core data store I create them:
if ([allGroupNodes count] == 0) {
JGGroupNode *rootTrainingNode = [JGGroupNode insertInManagedObjectContext:context];
[rootTrainingNode setNodeName:#"TRAIN"];
JGProjectNode *childUntrainedNode = [JGProjectNode insertInManagedObjectContext:context];
[childUntrainedNode setParent:rootTrainingNode];
[childUntrainedNode setNodeName:#"Untrained"];
JGGroupNode *rootBrowsingNode = [JGGroupNode insertInManagedObjectContext:context];
[rootBrowsingNode setNodeName:#"BROWSE"];
JGFolderNode *childFolder = [JGFolderNode insertInManagedObjectContext:context];
[childFolder setNodeName:#"Folder"];
[childFolder setParent:rootBrowsingNode];
[context save:nil];
}
What I Want
When I start the app, I want both top level groups to be expanded and "Untrained" to be highlighted as shown:
My Window http://synapticmishap.co.uk/Window.jpeg
The Problem
I put the following code in the applicationDidFinishLaunching: method of the app delegate:
[sourceListOutlineView expandItem:[sourceListOutlineView itemAtRow:0]];
[sourceListOutlineView expandItem:[sourceListOutlineView itemAtRow:2]];
NSIndexPath *rootIndexPath = [NSIndexPath indexPathWithIndex:0];
NSIndexPath *childIndexPath = [rootIndexPath indexPathByAddingIndex:0];
[self setSelectedIndexPaths:[NSArray arrayWithObject:childIndexPath]];
but the outline view seems to not have been prepared yet, so this code does nothing.
Ideally, eventually I want to save the last selection the user had made and restore this on a restart.
The Question
I'm sure it's possible using some crazy KVO to observe when the NSTreeController or NSOutlineView gets populated then expand the items and change the selection, but that feels clumsy and too much like a work around.
How would I do this elegantly?
Elegantly? This isn't elegant but it's how I'm doing it. I just do it manually. At app quit I write this value to user defaults:
lastSelectedRow = [outlineView selectedRow]
Then at app launch I run this in app did finish launching:
[self performSelector:#selector(selectLastNoteOrCreateDefaultNote) withObject:nil afterDelay:1];
Notice I just use a delay because I noticed the same as you that "the outline view seems to not have been prepared yet". Then in that selector I use this.
[outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:lastSelectedRow] byExtendingSelection:NO];
It works but better (more elegant) solutions are welcome from me too.
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
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))
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