April 15, 2006

Pimp My Code, Part 10: Whining about Cocoa

While I'm almost always a cheerleader for Cocoa (and Apple in general) in public, there are, of course, boneheaded things in Cocoa that grind at me every time I run up against them. Some annoyances exist for historical reasons (eg, Why does NSImage have twenty different ways to draw itself, each of which has slightly different semantics?), and some are apparently boundary problems between engineering groups (eg, Why doesn't NSImage have a way to create itself from a CGImageRef? Why are NSRect and CGRect exactly the same yet different? Why can't I ask an NSCachedImageRep for its CGContextRef?).

And, of course, some things are just plain old slap-your-head-duh mistakes...

Bindings, for instance, are my second-favorite feature in all of Cocoa (after CoreData). They're great. I love 'em. So much so that when any little part of them is less-than-perfect, it's incredibly grating.

NSTreeController is a new class designed to drive an NSOutlineView using bindings, which parallels NSArrayController's ability to drive an NSTableView. (Not surprisingly, they are both subclasses of NSController.) However, NSTreeController's design has a number of problems, most of which stem from this very questionable design decision (from Apple docs, emphasis mine):

- (id)arrangedObjects; // opaque root node representation of all displayed objects. This is just for binding to or passing around. At this time, developers should not make any assumption about what methods this object responds to.

Let me clarify this: if you hook an NSTreeController up to your object model, and you populate your tree with instances of class, say, "Shelf" (just picking a COMPLETELY random example), then the -arrangedObjects method will return... any guesses? Maybe, instances of class "Shelf"? Wrong! In fact, it returns objects of type "nothing-you-can-understand."

*Cough*Apoordesignsayswhat*cough*?

What the heck is going on here? Apparently someone at Apple decided it'd be easier to create strange little shadow objects that point to your REAL objects when you put real objects into an NSTreeController, but because that's such an ugly thing to do they decided not to document these little shadow objects, so you can't actually DO anything with them.

This is kind of like not signaling when making an illegal left turn, with the notion that if you don't signal it's not, you know, wrong.

Here are a bunch of problems that fell out from the the opaque-shadow-object design of NSTreeController:

• First, there is no -[NSTreeController setSelectedObjects:] method, because, of course, the objects YOU have access to are not the same ones that the ones that NSTreeController is messing with. So, uh, good luck setting the selection in code!

Oh, wait, there's -setSelectionIndexPaths:, which takes an 'NSIndexPath'... except there are two problems with this method, which unfortunately make it almost entirely useless. One is, NSIndexPath is a pretty obtuse class and it's always immutable, meaning if you want to build up a path to a particular object you're going to be writing a ton of code. The other is, if you change the NSTreeController's 'sortDescriptors' or its 'contents', any NSIndexPath you've socked away is invalidated, so you can't, say, store a selection and then re-create that selection later with any certainty.

Nor can you really select a particular object, because, uh, how do you know the path to your object when NSTreeController's opaque arrangedObjects can be in a different order than your source objects? Ok, sure, you can read off the sort order and replicate the sort on your objects, and build up a NSIndexPath to an object by also reading off the method NSTreeController is going to use to discover the children of your objects, as well... does this sound like a pain to you? Well, it is.

• Next, it's almost impossible to figure out how to get an NSTreeController to 'select none' or 'select all'. These should ideally take one line of code, and do with NSArrayController.

But when trying to select nothing on a NSTreeController, one cannot easily construct an NSIndexPath that is the empty set; all NSIndexPath creation methods are similar to +indexPathWithIndex:, which obviously creates a path that already has one entry (eg, is not empty), so you'd have to do something like [[NSIndexPath indexPathWithIndex:0] indexPathByRemovingLastIndex], which is fugly beyond all belief. If you call [NSIndexPath indexPathWithIndexes:NULL length:0] it crashes. (Possibly [[[NSIndexPath alloc] init] autorelease] works, I have to admit I didn't think of it until just now and it's very late and I've pretty much had it with screwing around with NSIndexPath.)

On the other hand, when trying to select all, you have to manually create an array containing a butt-ton of NSIndexPaths to cover every node in your tree! Ugh! And, since NSTreeController returns an OPAQUE objects, you again have to re-create the sorting and children-getting that NSTreeController is going to use when you're creating your array of indexPaths, which honestly takes a lot of the fun out of having an object in NIB.

• Similarly, almost all NSOutlineView delegate methods are almost useless, which means there's really no easy way to implement drag and drop. Consider the following NSOutlineView callbacks:

- (NSDragOperation)outlineView:(NSOutlineView*)outlineView
validateDrop:(id )info proposedItem:(id)item proposedChildIndex:(int)index;
- (NSDragOperation)outlineView:(NSOutlineView*)outlineView
validateDrop:(id )info proposedItem:(id)item proposedChildIndex:(int)index;
- (BOOL)outlineView:(NSOutlineView*)outlineView acceptDrop:(id )info
item:(id)item childIndex:(int)index;


Now consider that the 'item' parameters above are of an opaque class! Wait, what EXACTLY are you proposing? You'd like to drop this pasteboard onto an item in my NSOutlineView whose class is undocumented, so I have no idea where this drop is happening? Uh, let's see... Yes? No? Maybe? 3? Kentucky?

Similarly, it's hard to implement this NSOutlineView delegate method:

- (BOOL)outlineView:(NSOutlineView *)outlineView
shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item;


When, again, you can't actually look at the item that might be edited. I mean, if you don't actually CARE which PARTICULAR item is going to be edited, then you'd assumedly set the whole damn NSOutlineView to be editable or not, yes? But if you've gone to the trouble to implement this method, I'm guessing you are actually going to want to, you know, QUERY the 'item' a bit, ask about its family, see if it's been tested... make sure that it's really the kind of thing you want to be editing.

--

Ok, phew. I'm sure some engineer at Apple hates me now. (Sorry, overworked engineer dude. You can make fun of my code if you'd like.)

And, in fact, we have a nickname for people at Delicious who complain about stuff and don't do anything to make the situation better. So, for the record, I did, in fact, file a very detailed bug report with Apple on this in RADAR, so I'm not just complaining to the crowd - I honestly can't stand it when people report bugs in my stuff to the net instead of me, and I wouldn't do that to Apple.

But that's not enough, in my mind. I shouldn't just complain, I should fix this! Why don't I post some code that works around all these ills, so everyone can use NSTreeControllers the way they were intended, until Apple fixes the API in Liger (right, Apple?). Hey, good idea, me. I like the cut of your jib.

I have two categories here, one for NSOutlineView and one for NSTreeController. They contain different methods but some similar code, and I admit that I probably could merge their implementations a bit if I tried, but I also admit that there's only so much cleaning I'm going to do on code I've written to work around API limitations that I'm SURE will go away in the next release. But, please, if you do come up with better versions of these, post 'em in the comments or send 'em to me. They are appreciated!

First off, here's a category of 'NSTreeController' that adds a -setSelectedObjects: method which takes your REAL objects - not the phony shadow ones - from anywhere in your content tree, and also adds an -indexPathToObject:: method which will tell you where one of your REAL objects is in the NSTreeController given the controller's current sorting and children-getting-method and all that, so you don't have to get your hands all sticky with NSIndexPath gunk.

NSTreeController Category to work around annoyances
// NSTreeController-DMExtensions.h
// Library
//
// Created by William Shipley on 3/10/06.
// Copyright 2006 Delicious Monster Software, LLC. Some rights reserved,
// see Creative Commons license on wilshipley.com

#import <Cocoa/Cocoa.h>

@interface NSTreeController (DMExtensions)
- (void)setSelectedObjects:(NSArray *)newSelectedObjects;
- (NSIndexPath *)indexPathToObject:(id)object;
@end

// NSTreeController-DMExtensions.m
// Library
//
// Created by William Shipley on 3/10/06.
// Copyright 2006 Delicious Monster Software, LLC. Some rights reserved,
// see Creative Commons license on wilshipley.com

#import "NSTreeController-DMExtensions.h"

@interface NSTreeController (DMExtensions_Private)
- (NSIndexPath *)_indexPathFromIndexPath:(NSIndexPath *)baseIndexPath inChildren:(NSArray *)children
childCount:(unsigned int)childCount toObject:(id)object;
@end


@implementation NSTreeController (DMExtensions)

- (void)setSelectedObjects:(NSArray *)newSelectedObjects;
{
NSMutableArray *indexPaths = [NSMutableArray array];
unsigned int selectedObjectIndex;
for (selectedObjectIndex = 0; selectedObjectIndex < [newSelectedObjects count];
selectedObjectIndex++) {
id selectedObject = [newSelectedObjects objectAtIndex:selectedObjectIndex];
NSIndexPath *indexPath = [self indexPathToObject:selectedObject];
if (indexPath)
[indexPaths addObject:indexPath];
}

[self setSelectionIndexPaths:indexPaths];
}

- (NSIndexPath *)indexPathToObject:(id)object;
{
NSArray *children = [self content];
return [self _indexPathFromIndexPath:nil inChildren:children childCount:[children count]
toObject:object];
}

@end


@implementation NSTreeController (DMExtensions_Private)

- (NSIndexPath *)_indexPathFromIndexPath:(NSIndexPath *)baseIndexPath inChildren:(NSArray *)children
childCount:(unsigned int)childCount toObject:(id)object;
{
unsigned int childIndex;
for (childIndex = 0; childIndex < childCount; childIndex++) {
id childObject = [children objectAtIndex:childIndex];

NSArray *childsChildren = nil;
unsigned int childsChildrenCount = 0;
NSString *leafKeyPath = [self leafKeyPath];
if (!leafKeyPath || [[childObject valueForKey:leafKeyPath] boolValue] == NO) {
NSString *countKeyPath = [self countKeyPath];
if (countKeyPath)
childsChildrenCount = [[childObject valueForKey:leafKeyPath] unsignedIntValue];
if (!countKeyPath || childsChildrenCount != 0) {
NSString *childrenKeyPath = [self childrenKeyPath];
childsChildren = [childObject valueForKey:childrenKeyPath];
if (!countKeyPath)
childsChildrenCount = [childsChildren count];
}
}

BOOL objectFound = [object isEqual:childObject];
if (!objectFound && childsChildrenCount == 0)
continue;

NSIndexPath *indexPath = (baseIndexPath == nil) ? [NSIndexPath indexPathWithIndex:childIndex]
: [baseIndexPath indexPathByAddingIndex:childIndex];

if (objectFound)
return indexPath;

NSIndexPath *childIndexPath = [self _indexPathFromIndexPath:indexPath inChildren:childsChildren
childCount:childsChildrenCount toObject:object];
if (childIndexPath)
return childIndexPath;
}

return nil;
}

@end

Believe me, -setSelectedObjects: is handy as heck. Sewiously.

Next up, an NSOutlineView category that adds one crucial method: -realItemForOpaqueItem:, so when you're given an item from bizarro-world in an NSOutlineView delegate or datasource callback, you can just turn it into one of your objects, and speak to it in your language. You can thank me later! (Or, hell, thank me now; honestly, I'm not super-particular about the whole 'thanking' thing.)

NSOutlineView Category to work around NSTreeController
// NSOutlineView-DMExtensions.h
// Library
//
// Created by William Shipley on 3/10/06.
// Copyright 2006 Delicious Monster Software, LLC. Some rights reserved,
// see Creative Commons license on wilshipley.com

#import <Cocoa/Cocoa.h>

@interface NSOutlineView (DMExtensions)
- (void)setSelectedObjects:(NSArray *)newSelectedObjects;
- (NSIndexPath *)indexPathToObject:(id)object;
@end

// NSOutlineView-DMExtensions.m
// Library
//
// Created by William Shipley on 3/10/06.
// Copyright 2006 Delicious Monster Software, LLC. Some rights reserved,
// see Creative Commons license on wilshipley.com

#import "NSOutlineView-DMExtensions.h"

@interface NSOutlineView (DMExtensions_Private)
- (NSTreeController *)_treeController;
- (id)_realItemForOpaqueItem:(id)findOpaqueItem outlineRowIndex:(int *)outlineRowIndex
items:(NSArray *)items;
@end


@implementation NSOutlineView (DMExtensions)

- (id)realItemForOpaqueItem:(id)opaqueItem;
{
int outlineRowIndex = 0;
return [self _realItemForOpaqueItem:opaqueItem outlineRowIndex:&outlineRowIndex
items:[[self _treeController] content]];
}

@end


@implementation NSOutlineView (DMExtensions)

- (NSTreeController *)_treeController;
{
return (NSTreeController *)[[self infoForBinding:contentAttributeKey]
objectForKey:@"NSObservedObject"];
}

- (id)_realItemForOpaqueItem:(id)findOpaqueItem outlineRowIndex:(int *)outlineRowIndex
items:(NSArray *)items;
{
unsigned int itemIndex;
for (itemIndex = 0; itemIndex < [items count] && *outlineRowIndex < [self numberOfRows];
itemIndex++, (*outlineRowIndex)++) {
id realItem = [items objectAtIndex:itemIndex];
id opaqueItem = [self itemAtRow:*outlineRowIndex];
if (opaqueItem == findOpaqueItem)
return realItem;
if ([self isItemExpanded:opaqueItem]) {
realItem = [self _realItemForOpaqueItem:findOpaqueItem outlineRowIndex:outlineRowIndex
items:[realItem valueForKeyPath:[[self _treeController] childrenKeyPath]]];
if (realItem)
return realItem;
}
}

return nil;
}

@end

Let me finish by saying that I really don't like being negative towards Apple, both because I actually don't enjoy picking on other people's work unless they ask me to do it, and because Apple engineers have done so much incredible stuff that I would never have thought of myself, so it seems disingenuous to complain if some little bit of it is non-optimal.

On the other hand, I also don't want to come off as an indiscriminate cheerleader for anything and everything with Apple's imprimatur; that wouldn't help me or Apple or anyone who is considering following my advice. If an API is bad, I need license to say that it's bad, because otherwise it means nothing when I say that something good is good.

But on the OTHER other hand (the gripping hand, as it were), I know that Apple's detractors tend to seize upon any scrap of criticism that one of the faithful might express towards Apple and blow it out of proportion - witness the recent spate of witless articles about the supposed "backlash" (their term) against Boot Camp, which is really about as tight a product as you can get and is free and IS A FREAKING BETA, AND IF YOU HAVE A PROBLEM WITH THE NOTION OF PUTTING WINDOWS ON YOUR MAC JUST DON'T DO IT, DON'T START A FREAKING PETITION LIKE A WHINEY LITT...

Ok, I'm calm now. At any rate, please don't quote me as saying, "I've had it with Cocoa! Apple sucks!" Cocoa and bindings and CoreData are 90% of what makes Delicious Library 2 so tiggity-tight, and I simply could not have created Library 1 or 2 without them. Period. I won't port to Windows because I can't, not because I hate people who use Windows. It's just too hard. It's not like people TRY to make bad software for Windows. It ends up bad because if you have to spend all your time just fighting to get the most basic functionality working, of COURSE you're not going to have any time to polish.

So, much love to my homies on the bindings team. Love to all my Cocoa brothers and sisters.

And, please, fix NSTreeController for Liger. Sewiously!

Labels:

42 Comments:

Anonymous Matthias Winkelmann said...

A million thanks! I've been putting of some UI work because I hated the idea of tackling all these problems which seemed so pointless for me to deal with, since almost every developer seems to be suffering from them. Can't wait to try out your code.

And next time Apple offers you a job, could you please accept it? The cocoa community needs you in power.

April 15, 2006 3:50 AM

 
Blogger Wil Shipley said...

Apple has never actually offered me a job...

Wait, that's not true any more, I got offered an evangelist job, but that's not for me. I gots to code. I'll evangelize for free!

But I've never been offered an engineering job. I think they know I'm not really willing to take the pay cut. Plus, you know, HUGE EGO. Who wants to deal with that?

April 15, 2006 3:55 AM

 
Anonymous Anonymous said...

I can't wait to see DL 2.0 because the 1.0 version confirmed that Bindings were slow.

Now, it will be damn interesting to see a real app using CoreData and see whether it's slow or not.

April 15, 2006 4:46 AM

 
Anonymous Anonymous said...

Wil,

Do you allow the user to re-arrange objects in your tree via drag and drop? Do you know of a workaround for Frank's problem?

http://www.cocoadev.com/index.pl?NSTreeControllerBugOrDeveloperError

In my own test apps, and in his, messages are sent to zombies when rearranging more than one object in the tree at a time.

-J

April 15, 2006 5:35 AM

 
Blogger Aaron Tait said...

Thank you so much for saying what all of us are thinking! Having something as great as Core Data, and yet having something as boneheaded as NSTreeController's implementation is kind of like shipping an amazing laptop with the world's most advanced operating system and the creme de le crem of mobile processors only to have it come with a single button trackpad! I guess Apple will always be Apple.

Thanks for the code too! It's people like you, and apps like Delicious Library that make the Mac! I'm just starting to develop my app's UI and I planned on using an NSTreeController. Not only do I have a heads up now, I have an actually solution!

Thanks again,
-Aaron Tait (of Phine Technology a soon to be Mac shareware company)

April 15, 2006 5:52 AM

 
Blogger John Siracusa said...

Did you see JWZ's recent lament on a similar topic? He's not experienced with Cocoa at all (coming from an X/Unix background) but he's already frustrated with arrangedObjects. Here's one of the comments (from "wootest"):

"I'm fairly sure that the people who designed the bindings system were at the very least criminally insane. The big problem with bindings is that they aren't as of yet very flexible, and that a lot of controls simply have poor bindings support (like NSMatrix). Luckily, OS X 10.4 has shown some very tangible improvements over 10.3 when it comes to bindings, and I hope 10.5 continues down this road in the fall."

April 15, 2006 6:14 AM

 
Anonymous Gordon Tyler said...

I would imagine that the second time this line appears:

@implementation NSOutlineView (DMExtensions)

It should in fact be:

@implementation NSOutlineView (DMExtensions_Private)

And kudos for the gripping hand reference ;)

April 15, 2006 7:47 AM

 
Anonymous Anonymous said...

> Why can't I ask an NSCachedImageRep for its CGContextRef?

[[[cachedImageRep window] graphicsContext] graphicsPort]

April 15, 2006 10:47 AM

 
Blogger -John said...

Thanks for the great post... The timing is amazing given not two weeks ago I was staring at the docs for NSTreeController and saw the part about "you can't depend on what arranged objects being anything"

It was that exact moment I gave up on the outline view (well that and realization the fancy-pants search predicate foo doesn't work with it)

April 15, 2006 12:16 PM

 
Anonymous Anonymous said...

What's wrong with [shadowObject valueForKey:@"observedObject"] ?

April 15, 2006 2:47 PM

 
Anonymous Anonymous said...

JWZ is a poseur. He shouldn't assume that he's so 1337 that he doesn't need to RTFM.

If he'd even skimmed through the concept docs on CoreData, he'd know what "arrangedObjects" is.

April 15, 2006 10:36 PM

 
Blogger Lee said...

I personally like for my fellow developers to have ego. They tend to know more about their code. They tend to not settle for bad design/practice in their work.

People tend to think ego is bad for teamwork. But that is not true. It's just bad for mediocre teamwork.

April 16, 2006 6:12 AM

 
Anonymous Mark Stultz said...

What do you know about Cocoa programming? :P

But seriously, damn you Wil! You need to set up a separate site for all of the code on your blog. Going through archives is not very fluid. It doesn't have to be graphically impressive. No. At least an index of the code and maybe a blurb about it.

Pretty please.


Oh. And instructions for how we can make our very own sock puppet declioius monsters. I need one.

April 16, 2006 8:56 PM

 
Blogger Wil Shipley said...

Mark: We're selling 'em for $50 each. Steep, sure, but they're each hand-made and unique. We've got 8 to pick from right now.

April 16, 2006 8:58 PM

 
Blogger jkp said...

"What's wrong with [shadowObject valueForKey:@"observedObject"] ?"

WTF??? where did that come from? this is at best utilising a hidden property for the object (which i would imagine is an undocumented class anyway....)

April 17, 2006 12:59 AM

 
Anonymous Karl Adam said...

jkp: Actually -observedObject is a method on the shadow object itself. Which is a _NSArrayControllerTreeNode, so yes, Wil's method is esoteric and slow to be safe, but you could just as easily just ask for -observedObject. Anonymous's method is in a bad place in between.

As to the rest, I had the same probs and wrote similar solutions, except mine sends KVO notifs for selectedObjects.

April 17, 2006 7:33 AM

 
Anonymous Mark Stultz said...

Oh, and one more thing while I'm whining about your whining.

Can we get some proportionally spaced fonts in your CSS for code-cell? The helvetica seems obnoxious.

I'm sure I can't be the only one who thinks this. Although, I must admit... my Xcode code font is Webdings.

April 17, 2006 7:31 PM

 
Blogger Wil Shipley said...

I always code in Helvetica, to be honest. It is proven to be easier to read.

There's really no reason to have characters line up with the ones on the line above, as long as the indenting is consistent.

It's certainly a style thing, and you're welcome to do things however you like, but since this blog is about my style, I'm going to keep it.

April 17, 2006 7:38 PM

 
Anonymous Anonymous said...

No, I think Wil's right about ego being a limiter. The AppKit personnel tend not to have such huge egos (they used to be only one or two steps away from Steve Jobs at NeXT, so you can imagine why)

On the other hand, certain segments of the Carbon team had huge egos (tempered by massive intelligence, engineering experience, and usually being right).

April 17, 2006 8:00 PM

 
Anonymous Anonymous said...

Suddenly I'm very nostaligic for RTF source code. That was something I rather liked on NeXTSTEP.

-jcr

April 18, 2006 4:59 AM

 
Anonymous Scott Stevenson said...

I'm not totally sure, but I think I might have been the first one to break out class dump back in the WWDC 2004 days and try to figure out what to do with that "item" brick that NSTreeController passes in.

After Tiger came out, I distributed this little nugget to cocoa-dev and also did a blog post about it. When WWDC 2005 came around, I realized I might have made life difficult for somebody at Apple by encouraging use of undocumented API.

So I tracked down the guy responsibile for NSTreeController and did sort of a combo "I'm sorry for telling people to do this, what should they do instead?" thing. He said there really is no other solution, and he actually apologized to me for the situation.

I think the reality here is that the only software without bugs is software that never ships. Yes, it's slightly inconvenient, but at the end of the day we add a category and move on. Still vastly better than implementing all the data source methods manually.

April 18, 2006 7:38 PM

 
Anonymous Peter Maurer said...

I've always felt like a retard for sticking to my old hand-made data source methods instead of using NSTreeController et al.

Now, I may still be a retard, but I seem to have saved myself from a couple of headaches ;-)

April 19, 2006 11:36 AM

 
Anonymous Jesper said...

a) Great post. Great code. Thanks a lot.

b) A clarification to John Siracusa's comment - I was "wootest" in that thread:

I was trying to counter the argument of another comment that the bindings system is messy - it *is* hard to grasp at the beginning because of the sheer world of KVx concepts and the documentation's expectation that you'll be able to take them all on at once.

However, bindings is a brave thing to attempt (to code on Apple's part, not to implement in an app), it's not badly solved and it'll probably grow to be very flexible and well-integrated soon enough. (Also, as opposed to the parent comment in the original thread, I do code this stuff, and I'm not far off from being able to make a living off of it.)

April 20, 2006 1:06 PM

 
Anonymous Frederik Seiffert said...

I wonder about the creative-commons license you chose, which says "No Derivative Works". If I understand it correctly, it basically doesn't permit us to use in any project ("You may not [...] build upon this work"). The "Noncommercial" flag is also kind of limiting. Any comment?

Other than that, thanks for the great post! :)

May 13, 2006 6:26 AM

 
Blogger Wil Shipley said...

I'm intending to put up a separate CC license for the code samples -- that badge is supposed to only apply to the text of my posts, not the code.

Sorry for the confusion.

May 17, 2006 2:30 AM

 
Blogger Andrew Garber said...

Why are NSRect and CGRect exactly the same yet different?

Because Foundation and CoreGraphics are two mutually exclusive APIs. CoreGraphics can be used by both Cocoa and Carbon programmers. Since the two rects are identical C structs (according to Apple's documentation), you should be able to typecast them back and forth without any problems.

May 20, 2006 12:50 PM

 
Blogger Andrew Garber said...

This comment has been removed by a blog administrator.

May 27, 2006 9:13 PM

 
Blogger Andrew Garber said...

This comment has been removed by a blog administrator.

May 27, 2006 9:24 PM

 
Blogger Andrew Garber said...

Addendum to my previous post:

Correction: you should be able to typecast pointers to them without any problems. For example,

NSRect nsRect = NSMakeRect(1, 1, 1, 1);
CGRect cgRect = *((CGRect *)&nsRect);

May 27, 2006 9:26 PM

 
Anonymous George said...

I've been modifying these a bit to (hopefully) work with Core Data... namely because valueForKey:childrenKeyPath will return an NSSet instead of an NSArray.

Anyhoo, in _realItemForOpaqueItem, why'd you use a pointer for outlineRowIndex rather than a normal variable? My newb mind can't figure it out. I found it wasn't sending the correct row index for children so I changed it to a normal int and it seems to work fine (I also added an increment to it after the isItemExpanded check).

Thanks.

June 15, 2006 6:40 AM

 
Blogger George said...

OK, I see why a pointer is needed. We need it to increment every time regardless of recursion. Problem is, it gets updated at the END of the loop. So if children are involved, it recurses without the incremented value.

For instance:
item#1->item#2->item#3
item#4->item#5->item#6
(which would be represented on 6 rows)

I'm seeing the outlineRowIndex go 0,0,0,3,3,3

So I moved the (*outlineRowIndex)++ out of the For statement and made it the first line below. You then have to change its start value from 0 to -1.

Ok, this time I'm 95% sure this change is for the better. :)

June 15, 2006 7:37 AM

 
Anonymous Anonymous said...

I am new to Cocoa, so please be gentle :).
I have copied the four files into my project unmodified, but when I compile them I get an error in the NSOutlineView-DMExtensions.m file, line 33. The error reads as "error: 'contentAttributeKey' undeclared (first use in this funtion)". Is there something I am missing? Your method, -realItemForOpaqueItem: is exactly what I need right now! Please help .:)

July 26, 2006 10:51 PM

 
Blogger Wil Shipley said...

Oh, that should just be:

@"content"

or:

NSContentBinding

on 10.4 and later.

July 27, 2006 12:00 AM

 
Anonymous Anonymous said...

I am pretty new to cocoa, so please be gentle. I am confused about your code: for NSOutlineView shouldn't it be

@interface NSOutlineView (DMExtensions)
- (id)realItemForOpaqueItem:(id)opaqueItem;
@end

instead of

@interface NSOutlineView (DMExtensions)
- (void)setSelectedObjects:(NSArray *)newSelectedObjects;
- (NSIndexPath *)indexPathToObject:(id)object;
@end

August 22, 2006 8:29 AM

 
Blogger MacManitou said...

I just looked into apples the NSTreeController API documentation and realized that a simple -setSelectionForObject: method is missing. So I checked some of the cocoa developer blogs and came across your post - which is really good ;).

After reading it I dropped your code into my project and voila - it did not work instantly :(. It seems to be a problem with

id *childObject = [children objectAtIndex:childIndex];

You define it as NSArray, but in my project it ends up as NSMutableSet. That is much more confusing as I changed your code to

NSLog([children description]);
id *childObject;
// childObject = [children objectAtIndex:childIndex];

and got back a NSArray.

So that is the output on the console:
*** -[_NSFaultingMutableSet objectAtIndex:]: selector not recognized [self = 0x166f0e0]

I am pretty sure that NSMutableSet has no -objectAtIndex: method ;)

A confused Sascha

August 27, 2006 9:18 AM

 
Blogger Wil Shipley said...

Sascha:

There's a limitation in my code where you have to pass it an ordered set object (like NSArray) right now. This is just because my data was already set up.

But, it'd be trivial for you to grab the content (an NSSet in your case) and sort it using the NSController's sort criteria. I'll leave this as an exercise, since, uh, I actually ditched NSTreeController because I couldn't get it to work due to crashers in its selection code after removing two objects from my contents. (Reported RADAR bug, but seems as though it won't get fixed until Leopard, and I can't wait.)

August 27, 2006 12:17 PM

 
Blogger MacManitou said...

Hi Will,

I got it finally working - it would have bugged me the whole night, if this problem woulf have been still in my mind ;).

I used the NSTreeController extension from Apple and wrote the badest source code I have ever written - cudos to all my java folks at work that strangle me with there source ;)

int row;
row = [searchTableView selectedRow];

if (row != -1) {
//do stuff for the selected row
NSString *selectedTitle = [[searchResultsArray objectAtIndex:row] objectForKey:@"title"];
// using apples treecontroller extensions
NSArray *items = [notepadTreeController childObjectsByDepthFirstSearchStartingAt:nil];
NSEnumerator *enumerator = [items objectEnumerator];
id item;

// okay first lets get the object for the string
while ( item = [enumerator nextObject] )
if([selectedTitle isEqualToString:[item valueForKey:@"title"]]) break;

if(item) {
NSIndexPath *selectedIndexPath = [notepadTreeController arrangedIndexPathForObject: item startingAt: nil];
[notepadTreeController setSelectionIndexPath:selectedIndexPath];
}

It gets the selection from a table, looks up the right object for the string and gets back the path, which it sets as current selection on the tree - done! ;)

Feel free to pimp it - Kind regards,
Sascha

August 28, 2006 3:25 PM

 
Blogger Mathieu said...

Thanks for this code!

I had the same error as reported above and did your little 'exercise'. It was trivial but it took a while for me to find a stupid mistake (I autocompleted the wrong method without checking - ever done that before?)

So as to save other newbies out there like me (and everyone's a newbie compared to Wil!) here's the modified class implementation.

So if you're using a Core data entity which references itself, this code will turn the set into an array (to conform with Wil's code) and also sort it based on the controller's own sort descriptors.

@implementation NSTreeController (DMExtensions_Private)

- (NSIndexPath *)_indexPathFromIndexPath:(NSIndexPath *)baseIndexPath inChildren:(NSArray *)children
childCount:(unsigned int)childCount toObject:(id)object;
{
unsigned int childIndex;
for (childIndex = 0; childIndex < childCount; childIndex++) {
id childObject = [children objectAtIndex:childIndex];

NSMutableSet *childsChildren = nil;
unsigned int childsChildrenCount = 0;
NSString *leafKeyPath = [self leafKeyPath];
if (!leafKeyPath || [[childObject valueForKey:leafKeyPath] boolValue] == NO) {
NSString *countKeyPath = [self countKeyPath];
if (countKeyPath)
childsChildrenCount = [[childObject valueForKey:leafKeyPath] unsignedIntValue];
if (!countKeyPath || childsChildrenCount != 0) {
NSString *childrenKeyPath = [self childrenKeyPath];
childsChildren = [childObject mutableSetValueForKey:childrenKeyPath];
if (!countKeyPath)
childsChildrenCount = [childsChildren count];
}
}

BOOL objectFound = [object isEqual:childObject];
if (!objectFound && childsChildrenCount == 0)
continue;

NSIndexPath *indexPath = (baseIndexPath == nil) ? [NSIndexPath indexPathWithIndex:childIndex]
: [baseIndexPath indexPathByAddingIndex:childIndex];


if (objectFound)
return indexPath;

//change NSSet to NSArray and sort
NSMutableArray *childsChildrenAsArray = [NSMutableArray arrayWithArray:[childsChildren allObjects]];
[childsChildrenAsArray sortUsingDescriptors:[self sortDescriptors]];

NSIndexPath *childIndexPath = [self _indexPathFromIndexPath:indexPath inChildren:childsChildrenAsArray
childCount:childsChildrenCount toObject:object];
if (childIndexPath)
return childIndexPath;
}

return nil;
}

@end

// I had to get my hands dirty after all ;)

November 04, 2006 9:41 PM

 
Anonymous Bartek said...

Amazing code!! Thanks!! But one addition has to be made to a method when using Core Data. Your content must also be turned to an array! It's a NSSet by default!

- (NSIndexPath *)indexPathToObject:(id)object;
{


NSMutableArray *children = [NSMutableArray arrayWithArray:[self content]];
[children sortUsingDescriptors:[self sortDescriptors]];

return [self _indexPathFromIndexPath:nil inChildren:children childCount:[children count]
toObject:object];

}

April 06, 2007 3:01 AM

 
Anonymous Ken Hornak said...

I'm surprised no one has commented on this but in _indexPathFromIndexPath: shouldn't the section:

if (countKeyPath)
childsChildrenCount = [[childObject valueForKey:leafKeyPath] unsignedIntValue];

really read:

if (countKeyPath)
childsChildrenCount = [[childObject valueForKey:countKeyPath] unsignedIntValue];

?

You want to get the number of children in childObject so you should call countKeyPath and not leafKeyPath, correct?

July 30, 2008 10:59 AM

 
Blogger Nolan Waite said...

Big thanks to Wil and commentators for the code.

George was posting about the -realItemForOpaqueItem: method earlier, mentioning some changes he made for Core Data (children coming in NSSets, not arrays). Here are the relevant changes I made, which seem to work fine (changes highlighted in bold):

1. In the implementation of -realItemForOpaqueItem, the first line changes to:

int outlineRowIndex = -1;

2. In the implementation of -_realItemForOpaqueItem:::

The for loop changes to:

for (itemIndex = 0; itemIndex < [items count] && *outlineRowIndex < [self numberOfRows]; itemIndex++ /* snipped: && (*outlineRowIndex)++ */) {
(*outlineRowIndex)++;
id realItem = [items objectAtIndex:itemIndex];
id opaqueItem = [self itemAtRow:*outlineRowIndex];
if (opaqueItem == findOpaqueItem)
return realItem;
if ([self isItemExpanded:opaqueItem]) {
id items = [realItem valueForKeyPath:[[self _treeController] childrenKeyPath]];
if ([items isKindOfClass:[NSSet class]])
items = [items allObjects];

realItem = [self _realItemForOpaqueItem:findOpaqueItem outlineRowIndex:outlineRowIndex
items:items];
if (realItem)
return realItem;
}
}

It might be smarter to sort the children array I guess, but this works for me.

September 26, 2008 11:56 PM

 
Blogger Ken Hornak said...

So I finally got around to using this code for an outline view that actually had a hierarchy--nested items--only to find that it didn't work properly. Previous comments were close in how to fix it but still weren't correct. I'm posting this here to save others the hours I wasted. I hope it works for you.

- (id)_realItemForOpaqueItem:(id)findOpaqueItem
outlineRowIndex:(int *)outlineRowIndex
items:(NSArray *)items
{
int numItems = [items count];
if ( numItems == 0 )
return nil;

int numOutlineRows = [self numberOfRows];

int itemsIndex;
for (itemsIndex = 0; itemsIndex < numItems && *outlineRowIndex < numOutlineRows ; itemsIndex++)
{
id opaqueItem = [self itemAtRow:*outlineRowIndex];
if ( opaqueItem == findOpaqueItem )
return [items objectAtIndex:itemsIndex];

++(*outlineRowIndex);

if ( [self isItemExpanded:opaqueItem] )
{
id realItem = [items objectAtIndex:itemsIndex];
NSArray* children = [realItem valueForKeyPath:[[self _treeController] childrenKeyPath]];
realItem = [self _realItemForOpaqueItem:findOpaqueItem
outlineRowIndex:outlineRowIndex
items:children];
if (realItem)
return realItem;
}
}

return nil;
}

January 15, 2010 2:41 PM

 

Post a Comment

<< Home