I Love Outline Views – Here’s Mine
Apps like Coda are the de-facto standard for good user-interface design nowadays. You can do a lot with the standard Cocoa controls provided by Apple, but you can’t always get exactly what you want out-of-the-box. For example, the CSS edit section of Coda has a fantastic way of dividing tasks: the animated outline view.
Rather than having a monolithic view that has all the controls grouped with dividing lines; each section, “Text”, “Colors and Background”, “Dimensions”, all exist in their own collapsible view. Clicking the separating bar animatedly expands or collapses the view.

To make one of these views isn’t quite as easy as you first think, and you can quickly go down the wrong road in the design. The super-secret trick is to *not* use Core Animation, or more precisely: ignore the lure of NSViews conformance to the NSAnimatablePropertyContainer protocol. As of Mac OS X 10.5, a few of the properties of a view or window can be animated simply be replacing calls like [view setFrame:newFrame]; with [[view animator] setFrame:newFrame];. This is fine when you have one or two views that you want to move but it’s impossible to coordinate the movements of multiple view using this API. After using Core Animation, one would expect that after a call such as [[view animator] setFrame:newFrame]; requesting the frame from the view would return newFrame. Unfortunatley it returns the original frame of the view. Only when the animation is done, do you get newFrame. Furthermore, just try setting the delegate of the CABasicAnimation object that handles the implicit animation and getting any form of useful information back. You can’t. You’re in for a world of pain if you try.
The solution: use good-ol’ NSViewAnimation. That way you can create all the dictionaries containing the animations for all the views you want to, then instantiate one NSViewAnimation object to handle the lot and set them off at the same time. Simple.
The Animating Outline View
The outline view is made up of a few classes, the TLDisclosureBar, a subclass of the highly-configurable TLGradientView; the TLCollapsibleView which has a TLDisclosureBar and any NSView you like as its subviews. A TLCollapsibleView itself can be used alone, and will animate the collapse/expansion of the NSView subview (which I term the “detail view”).
The fun part comes when TLCollapsibleViews are subviews of a TLAnimatingOutlineView. In this case, the TLAnimatingOutlineView takes over all the animations, simply asking the TLCollapsibleView that the user has selected for the NSDictionary object that describes the collapse/expansion animation. With this information, it constructs the movement animations for all the subviews below the collapsing/expanding view and handles the required change of frame size to accommodate the changing content size. It’s all quite elegant, if I do say so myself.
Not being content with example code, I’ve made sure these classes are real-world useful. This view is integral to BibTeX management in Scribbler so it needs to be good. Some of the relevant parts of the NSOutlineView API are replicated and the delegate of the TLAnimatingOutlineView gets will/did/expand/collapse notifications, along with allowing the delegate to deny/allow animations, too. Each of the TLCollapsibleViews will also ask their detail views if the collapse/expansion is allowed if the TLCollapsibleDetailView protocol is implemented. The TLAnimatingOutlineView itself works when in an NSScrollView.
The code, along with an example project, is hosted on Google Code here. It is distributed under the new BSD license.
So why is is not ESAnimatingOutlineView, why the TL namespace? Well, I’m working on something big, at the same time as writing Scribbler. Start guessing.
Update
I’ve fixed a few bugs in the code, the current version can be checked out of the repository. Fixes include: proper support for hiding subviews of the TLDisclosureBars when the outline view is resized small, a correction to the autoresizing mask of the TLAnimatingOutlineView itself, declaration of keys that the TLCollapsibleView supplies with the animation dictionaries, the delegate of the outline view and subclasses of TLGradientView are no longer unregistered for notifications that client code specifies (i.e. no longer uses [[NSNotifcationCenter defaultCenter] removeObserver:_delegate];).
Mark Aufflick 14:38 on October 25, 2008 Permalink |
So happy to hear about the bibtex support! Oh, the animated views will be nice too, and thanks for sharing the example code.
Jonathan Dann 17:52 on October 25, 2008 Permalink |
Hi Mark,
Thanks for the encouragement! I’m getting really excited about this app. I just hope that I can get it out asap
Glad you like the sample code too. If you do manage to use it, let me know.
Brad Gibbs 17:45 on January 14, 2009 Permalink |
Thanks for the sample code. It’s been very helpful!
The sample project failed to compile initially. I changed a #import from Espresso/TLGradientView to TLGradientView and it compiled and ran fine.
I’m trying to integrate your code into an app. I’ve copied and pasted the AppController class in your sample project to an NSViewController subclass in my project. The app compiles and runs, but crashes when I try to expand or collapse an item. The Debugger stops on this line:
if (![(id)self.delegate respondsToSelector:@selector(outlineView:shouldCollapseItem:)])
The sample code I downloaded gives a warning stating that AppController may not conform to the TLAnimatingOutlineViewDelegate protocol. I added to my outline view controller, which shuts up the compiler, but the app still crashes.
Any thoughts on what I might be doing wrong?
Code: Accordion View at Under The Bridge 06:47 on January 21, 2009 Permalink |
[...] there’s help now: over at espresso-served-here you can find what looks like a very slick implementation of the idea, Core Animation cool and [...]
Markus 23:45 on August 6, 2009 Permalink |
Great stuff. Downloaded yesterday and works perfectly. One minor issue is the delegate method names set in -setDelegate: of TLAnimatingOutlineView in TLAnimatingOutlineView.m
In your version they’re all tied to -outlineViewItemWillExpand:, they should be wired to Did/Will Expand/Collapse. This is the fix:
{
if (_delegate == delegate)
return;
[self _removeDelegateAsObserver];
_delegate = delegate;
if ([(id)_delegate respondsToSelector:@selector(outlineViewItemWillExpand:)])
[[NSNotificationCenter defaultCenter] addObserver:_delegate selector:@selector(outlineViewItemWillExpand:) name:TLAnimatingOutlineViewItemWillExpandNotification object:self];
if ([(id)_delegate respondsToSelector:@selector(outlineViewItemDidExpand:)])
[[NSNotificationCenter defaultCenter] addObserver:_delegate selector:@selector(outlineViewItemDidExpand:) name:TLAnimatingOutlineViewItemDidExpandNotification object:self];
if ([(id)_delegate respondsToSelector:@selector(outlineViewItemWillCollapse:)])
[[NSNotificationCenter defaultCenter] addObserver:_delegate selector:@selector(outlineViewItemWillCollapse:) name:TLAnimatingOutlineViewItemWillCollapseNotification object:self];
if ([(id)_delegate respondsToSelector:@selector(outlineViewItemDidCollapse:)])
[[NSNotificationCenter defaultCenter] addObserver:_delegate selector:@selector(outlineViewItemDidCollapse:) name:TLAnimatingOutlineViewItemDidCollapseNotification object:self];
}
Jonathan 08:02 on August 7, 2009 Permalink |
Excellent, I’ll file it as a TODO
Dan Pahlajani 01:33 on March 8, 2010 Permalink |
Hi Jonathan,
This is really fantastic code you have made available to write cool Cocoa apps and is greatly appreciated.
I also have a couple of questions. Currently, the views are being setup in the awakeFromNib which is perfect but causes a little flashing from the window opens. Is there way that the views can be set in advance to avoid the flashing?
Robert Payne 03:03 on April 30, 2010 Permalink |
I know this is a relatively old post but great work Jonathan,
I would like to note when using this code in a multi-window application ( I’m currently using it on every window for a document based application ) there is some huge performance hits when creating more than a single window.
The TLGradientView adds listeners to the window become active / resign active notifications and send those calls directly to “display” which results in massive performance hits.
The listeners should instead call [self setNeedsDisplay:YES]; instead of directly invoking the “display” command. This increases performance massively and my application can instead open 20 or so windows before feeling the performance hit that was occuring on the first window open.
Jonathan Dann 08:30 on April 30, 2010 Permalink |
Wow, I remember doing that now but I have no idea what I was thinking. You’re right, of course, -[NSView display] is almost always to be avoided.
Robert Payne 08:33 on April 30, 2010 Permalink |
Pretty easy fix. I was having performance issues for past week or so since I implemented the outline view ( which is AWESOME by the way great job! ) and found it was the outline view itself but it took awhile to find out the problem and then how to fix it directly.
Jonathan Dann 08:40 on April 30, 2010 Permalink |
Yeah I can imagine that one took some time. Glad you found it useful
Mark Aufflick 23:28 on May 20, 2010 Permalink |
Hi Jonathon,
a) there’s a minor display glitch when used in a height adjustable context in that the view can be sized smaller than the clip view and thus when the height is reduced (eg. resizing the window) the view get’s hidden behind grey nothingness. The fix is simple:
*** tlanimatingoutlineview-read-only/Classes/TLAnimatingOutlineView.m Sun May 16 17:33:10 2010
— ../cocoa/mine/DailyImageWorkflow/TLAnimatingOutlineView.m Thu May 20 22:20:23 2010
***************
*** 91,96 ****
— 91,103 —-
for (TLCollapsibleView *subview in [self subviews])
newViewFrame.size.height += NSHeight([subview frame]) + [self.delegate rowSeparation];
+
+ if ([self enclosingScrollView]) {
+ NSSize contentSize = [[self enclosingScrollView] contentSize];
+ if (newViewFrame.size.height < contentSize.height)
+ newViewFrame.size.height = contentSize.height;
+ }
+
[self setFrame:newViewFrame];
}
b) how far did you get with your image adjust view? IKImageView is killing me!
c) when is Scribbler coming out?!
Cheers,
Mark.
Mark Aufflick 23:57 on May 20, 2010 Permalink |
Also I have a patch for the display/setNeedsDisplay: issue – if you want to add me to the google code project I’ll make both changes. My google code username is “aufflick”.
Bharath 08:05 on March 24, 2011 Permalink |
Jonathan,
Thanks a lot for this AWESOME work. I was about to start this and was not sure where to start and where would I end. You saved AGES of my time