How to efficiently flatten a NSOutlineView? - objective-c

I have a NSOutlineView which is binded to my NSTreeController. The content of the NSTreecontroller (myTreeController) is set with data using the command:
[self.myTreeController setContent:self.myArrayOfFiles];
The content of the array consists of NSTreeNode parent and children objects that are added using a NSTreeNode subclass (TKnode):
[TKnode treeNodeWithRepresentedObject:myRootObject].
This works very well and displays my NSoutlineView correctly. But when I want to iterate over the content of the NSOutlineView I need to flatten it and store the objects in an array. This is where I could need some help as the solution I have is not optimal and potentially prone to errors.
I first return the content of the NSTreeController using:
- (NSArray *)rootNodes;
{
return [[myTreeController arrangedObjects] childNodes] ;
}
This returns a NSTreeControllerTreeNode array of size one that holds the tree structure. I then access the first layer of the tree structure using the childNodes method.
- (NSArray *)flattenedNodes
{
NSMutableArray *mutableArray = [NSMutableArray array];
for (TKnode *rootnode in [self rootNodes]){
for (TKnode *node in [rootnode childNodes]){
if (![[node representedObject] isLeaf]){
[mutableArray addObjectsFromArray:[self descendants:[node representedObject]]];
}
else {
[mutableArray addObject:[[node representedObject] representedObject]];
}
}
}
DLog(#"My files: %lu",[mutableArray count]);
return [[mutableArray copy] autorelease];
}
The children of the children are accessed recursively using the following method:
- (NSArray *)descendants:(TKnode *) node
{
NSMutableArray *descendantsArray = [NSMutableArray array];
for (TKnode *mynode in [node childNodes]) {
[descendantsArray addObject:[mynode representedObject]];
if (!mynode.isLeaf){
[descendantsArray addObjectsFromArray:[self descendants:mynode]];
}
}
return [[descendantsArray copy] autorelease]; // return immutable
}
This works and returns an array containing all of my objects, not the NSTreeNodes, but the objects I am interested in. But it seems error prone that I have to call representedObject sometimes twice on an object to access my object. Is this the way I am supposed to work with NSTReeController and NSTreeNodes? Is there a better way? The reason I wanted to use NSTreeNode and NSTreeController in the first place was to get some of the already implemented methods for free such as sorting and arrangedObjects, which I am used to with NSTableView. But the method I use to access the NSTreeController content does not seem correct ? Should I use the arrangedObjects to get the content of the NSTReeController or should I use a different approach? Any suggestions for how to correctly flatten a NSOutlineView is highly appreciated.
Thanks! Cheers, Trond

This question turned out to be a real tumbleweed (my second in one month) so I wanted to follow up with some more info. I ended up using the above posted code for flattening my NSOutlineView as I was unable to find a better option. When you have to recursively iterate through an unknown number of subfolders (in my case) this seems to be the best option where the descendants method is called for each time you reach a deeper level. I found a very useful website with a number of useful NSTreeController extensions here, which uses the same approach as I had taken. Unless someone helps me figure out a faster and better algorithm for flattening the arrangedObjects of a NSTreeController, I believe i will stick with this approach.
Cheers, Trond
Update: After a question for the solution I decided to post my method for how to flatten an outlineview. I hope his can help others.
- (void) flattenOutlineview
{
NSMutableArray *nodeArray = [NSMutableArray array];
[self.myFlattenedFiles removeAllObjects];
[self.myFlattenedNodes removeAllObjects];
[self.myIndexDict removeAllObjects];
[nodeArray removeAllObjects];
for (SDNode *rootnode in self.rootNodes)
{
[nodeArray addObject:rootnode];
[self.myIndexDict setObject:[rootnode indexPath]
forKey:[[[rootnode representedObject] representedObject] fullpathL]];
for (SDNode *node in [rootnode childNodes])
{
if (node.isLeaf){
[nodeArray addObject:node];
[self.myIndexDict setObject:[node indexPath]
forKey:[[[node representedObject] representedObject] fullPathCopy]];
}
else {
[nodeArray addObjectsFromArray:[self descendants:node]];
[self descendantsIndex:node];
}
}
}
[self.myFlattenedNodes setArray:[nodeArray copy]];
for (SDNode *node in self.myFlattenedNodes)
{
[self.myFlattenedFiles addObject:[[node representedObject] representedObject]];
}
}

Saw this linked from another answer I just wrote. Provided you have everything in the tree expanded/displayed this would work:
for (NSUInteger row = 0; row < [outlineView numberOfRows]; row++)
{
NSTreeNode* node = [outlineView itemAtRow:row];
[nodeArray addObject:node];
}
The rows of the outlineView are basically a flattened tree. And you could perhaps expand every item while iterating to get them all.

Related

Traverse a tree from root to all children in Objective-C

A simplified version of what I want to do is to turn a tree like this:
Into an array like this: ["abc","abd","ae"]
Basically I want to traverse the tree from it's root node to each of the children.
I've tried doing this by putting a for-in loop in a recursive block, but the problem was that the for loop would start over each time the block was recursed. When I tried running the block asynchronously I kept getting EXC_BAD_ACCESS
Any suggestions?
Say the tree is represented like this:
#interface TreeNode : NSObject
#property(weak,nonatomic) TreeNode *parent;
#property(strong,nonatomic) NSArray *children;
#end
The lineage (which is what you're looking for) of any node, is a list of nodes from the root to the node. This can be defined recursively like this:
- (NSArray *)lineage {
if (!self.parent) {
return #[self];
} else {
NSMutableArray *lineage = [[self.parent lineage] mutableCopy];
[lineage addObject:self];
return lineage;
}
}
You're looking for the lineages of the leaves, so we need a way to collect the leaves. We can do that if we can traverse the tree. This is a good application for blocks, like this:
- (void)depthFirst:(void (^)(TreeNode *))block {
for (TreeNode *node in self.children) {
[node depthFirst:block];
}
return block(self);
}
And that provides a natural way to collect leaves:
- (NSArray *)leaves {
NSMutableArray *leaves = [#[] mutableCopy];
[self depthFirst:^(TreeNode *node) {
if (!node.children) [leaves addObject:node];
}];
return leaves;
}
Putting it together, we get:
- (NSArray *)lineagesOfLeaves {
NSMutableArray lineages = [#[] mutableCopy];
for (TreeNode *leaf in [self leaves]) {
[lineages addObject:[leaf lineage]];
}
return lineages;
}
These methods work on any node in the tree. Though, for your question, you'll want to send lineagesOfLeaves to the tree's root.

NSOutlineView shows only first 2 levels of the NSTreeController hierarchy

I'm populating a NSOUtlineView with a NSTreeController.
The NSTreeController is a 3 levels hierarchy controller (CBMovie, CBDisc and CBEpisode), but only the first 2 levels are displayed in the outline view.
The implementation is the same for all objects: I've implemented methods to specify children, children count and if the object is a leaf. These methods are correctly called for all objects (also for those ones that are not displayed, the grandchildren: CBEpisode).
In the outline View, everything is displayed correctly for the first 2 level. But grandchildren are never displayed, I don't have the option to expand their parent to see them. I can only see CBMovie and CBDiscs.
I'm wondering if there is another setting I'm missing, about how deep the nodes can expand in NSTreeControllers or NSOutlineView configurations.
Below: Implementation in one of the three nodes.
Each node class has different path to its children. This is specified in the -(NSArray*)children method (correctly called).
-(NSArray*)children
{
return [[self Episodes] allObjects];
}
-(int)childrenCount
{
return [[self Episodes] count];
}
-(BOOL)isLeaf
{
return ![[self Episodes] count];
}
Output of logging code. The datasource, the NSTreeController, seems to have the correct structure.
CBMovie
CBDisc
CBEpisode
CBEpisode
CBMovie
CBDisc
CBDisc
CBDisc
CBDisc
CBMovie
CBDisc
CBEpisode
CBEpisode
This is how I populate the NSOutlineView (cell based). I don't use datasource methods, but I'm binding it programmatically.
NSMutableDictionary *bindingOptions = [[NSMutableDictionary alloc] initWithCapacity:2];
if (metadata.valueTransformer) {
[bindingOptions setObject:metadata.valueTransformer forKey:NSValueTransformerNameBindingOption];
}
[bindingOptions setObject:[NSNumber numberWithBool:NO] forKey:NSCreatesSortDescriptorBindingOption];
[bindingOptions setObject:[NSNumber numberWithBool:NO] forKey:NSRaisesForNotApplicableKeysBindingOption];
[newColumn bind:#"value" toObject:currentItemsArrayController withKeyPath:[NSString stringWithFormat:#"arrangedObjects.%#", metadata.columnBindingKeyPath] options:bindingOptions];
Examining the NSTreeController side
The NSTreeController is a 3 levels hierarchy controller, but only the first 2 levels are displayed in the outline view.
Have you confirmed that all three levels are loaded into your NSTreeController? You could do this by logging its contents to the console with the extension below. If the output produced by this code matches what appears in your outline-view, the problem is probably on the NSTreeController side, not the outline-view.
#import "NSTreeController+Logging.h"
#implementation NSTreeController (Logging)
// Make sure this is declared in the associated header file.
-(void)logWithBlock:(NSString * (^)(NSTreeNode *))block {
NSArray *topNodes = [self.arrangedObjects childNodes];
[self logNodes:topNodes withIndent:#"" usingBlock:block];
}
// For internal use only.
-(void)logNodes:(NSArray *)nodes
withIndent:(NSString *)indent
usingBlock:(NSString * (^)(NSTreeNode *))block {
for (NSTreeNode * each in nodes) {
NSLog(#"%#%#", indent, block(each));
NSString *newIndent = [NSString stringWithFormat:#" %#", indent];
[self logNodes:each.childNodes withIndent:newIndent usingBlock:block];
}
}
#end
The above code does not need to be adjusted, all you need to do is call it with a customised block:
-(void)logIt {
[self.treeController logWithBlock:^NSString *(NSTreeNode * node) {
// This will be called for every node in the tree. This implementation
// will see the object's description logged to the console - you may
// want to do something more elaborate.
NSString *description = [[node representedObject] description];
return description;
}];
}
Examining the NSOutlineView side
If all the data seems to have loaded correctly into the NSTreeController you could have a look at how your NSOutlineView is populated. The delegate method -[NSOutlineViewDelegate outlineView:viewForTableColumn:item] is a good place to start. The item argument is the NSTreeNode instance, so, before you return the relevant view you can look at this node and make sure it's behaving as expected. In your case, you should scrutinize the properties of item objects that are representing CBDisc objects. (You may need to click on disclosure buttons to get this method to fire for the relevant objects.)
-(NSView *)outlineView:(NSOutlineView *)outlineView
viewForTableColumn:(NSTableColumn *)tableColumn
item:(id)item {
NSTreeNode *node = (NSTreeNode *)item;
NSManagedObject *representedObject = (NSManagedObject *)node.representedObject;
// Query the node
NSLog(#"%# <%#>", representedObject.description, [representedObject class]);
NSLog(#" node is a leafNode: %#", node.isLeaf ? #"YES" : #"NO");
NSLog(#" node has child-count of: %lu", (unsigned long)node.childNodes.count);
// Query the NSManagedObject
// your stuff here...
// This is app-specific - you'll probably need to change the identifier.
return [outlineView makeViewWithIdentifier:#"StandardTableCellView" owner:self];
}
So, I've figured out why.
Quite stupid: the main outline column containing the disclosure arrows was 20px width only and the arrows of the children had indentation.
I'm using the outline column for the arrows only and not the titles of the nodes, that's why it's so narrow.
I had disabled indentation and now I can see all arrows.

Is there another way to filter subviews by class except enumerate and check?

First of all, by filtering, means finding all the (targetClass) views in the subviews.
Normally, if we want to filter subviews by class, we probably do:
// Create an array to hold them
NSMutableArray *filteredViews = [NSMutableArray new];
// enumerate and check
for (UIView *view in parentView.subviews) {
if ([view isMemberOfClass:[targetClass class]) {
[filteredViews addObject:view];
}
}
Is this the proper way to do it?
Does Cocoa Touch have a dedicated method to filter subviews?
You can use NSPredicate to define a rule to select the items you want. Note I'm using isKindOfClass, and not isMemberOfClass, since the former takes class hierarchy into account, and will be able to identify subclasses of the class you are looking for.
NSPredicate* predicate = [NSPredicate predicateWithFormat:#"self isKindOfClass: %#", [targetClass class]];
NSArray* filteredViews= [parentView.subviews filteredArrayUsingPredicate:predicate];

Outlets not working after showing "old" view again

I have several UIViews in my Storyboard and, of course, I can switch between them using a segue. Initially this works just fine:
notenKurse is a NSMutableArray, and kurse1Outlets is an outlet collection with my UITextFields.
int counter = 0;
for (UITextField *tf in kurse1Outlets) {
NSMutableString *t = [NSMutableString stringWithFormat:#"%#", [notenKurse objectAtIndex:counter]];
NSLog(#"Object at index %i is %#", counter, [notenKurse objectAtIndex:counter]);
if ([t isEqualToString:#"42"]) {
[t setString:#""];
}
[tf setText:t];
NSLog(#"UITextField in slot %i should now display %#", counter, t);
counter++;
}
All of my UITextFields are displaying the value stored in the array. But if I go to another view (let's assume I have a Button for it ;) ) Change something, and then go back to the original UIView the above code still gets executed, and there are different values in the array (this is supposed to be). I can see that in the log. But the stupid UITextField just doesn't display anything. Neither what was in there before, nor the new text. But why? The log clearly shows that t is what it's supposed to be, so the error must be in writing it into the textfield, and therefore I guess it's an outlet issue...
There is no guarantee of the order of your outlet collection. It's treated very much like an NSDictionary as opposed to an NSArray - where order is guaranteed. Iterating over this sort of collection will yield different results for different devices/people/phase of the moon.
When I use a collection like this I tend to set the 'tag' and then reorder the outlet collection when viewDidLoad by sorting off of the tag.
self.calendarDayImageViews = [_calendarDayImageViews sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
if ([(UIView *)obj1 tag] < [(UIView *)obj2 tag]) {
return NSOrderedAscending;
}
else if([(UIView *)obj1 tag] > [(UIView *)obj2 tag]){
return NSOrderedDescending;
}
else{
return NSOrderedSame;
}
}];
You can just output the tf,by
NSLog(#"%#",tf);
To check if the tf is null
Ok, i found it. I forgot to release some stuff, and so my UITextFields did get set before the array was sorted. My mistake!

How can I iterate through all items of my NSOutlineView?

I need to perform an operation on all objects in my NSOutlineView when I close the window.
(Both parents and children, and children of children).
It doesn't matter if the items are expanded or not, I just need to perform a selector over all items in the outline view.
thanks
Assuming you're using NSOutlineViewDataSource and not bindings, you could do it like this:
- (void)iterateItemsInOutlineView: (NSOutlineView*)outlineView
{
id<NSOutlineViewDataSource> dataSource = outlineView.dataSource;
NSMutableArray* stack = [NSMutableArray array];
do
{
// Pop an item off the stack
id currentItem = stack.lastObject;
if (stack.count)
[stack removeLastObject];
// Push the children onto the stack
const NSUInteger childCount = [dataSource outlineView: outlineView numberOfChildrenOfItem: currentItem];
for (NSUInteger i = 0; i < childCount; ++i)
[stack addObject: [dataSource outlineView: outlineView child: i ofItem: currentItem]];
// Visit the current item.
if (nil != currentItem)
{
// Do whatever you want to do to each item here...
}
} while (stack.count);
}
This should achieve a full traversal of all the objects vended by your NSOutlineViewDataSource.
FYI: If you're using cocoa bindings, this won't work as is. If that's the case, though, you could use a similar approach (i.e. surrogate-stack traversal) against the NSTreeController (or whatever else) you're binding to.
HTH