Apple’s Keynote has a cool and efficient UI paradigm on application start-up. It displays a panel with a collection of themes from which to start a new presentation, but also has a button on the panel to choose an existing presentation. Pressing the choose existing presentation button performs a panel flip animation to reveal the common system open panel. Canceling from the open panel results in a flip animation back to the theme chooser panel.

Start-up screens are ubiquitous, but I haven’t seen the addition of the open existing button before. It’s a small thing, but it saves the user the annoyance of having to hit cancel and then File/Open. The flip has also got that cool gratuitous Apple bling thing going on.
I’d implemented the flip effect using Core Animation previously in the context of a small item embedded in a larger super view, but that was flipping CALayers, not NSWindows. Also, we’re talking about messing with modal windows to boot, including one that isn’t real sub-class friendly: NSOpenPanel.
A quick consultation with Google turned up two different existing implementations of flipping NSWindows:
- WindowFlipper by Uli Kusterer in 2005
- Flipr by Rainer Brockerhoff in 2006
Both look well implemented and are conveniently packaged up as a category on NSWindow. Both seemed to be showing their age a little though as they chug based on their image manipulation technology. It also wasn’t immediately obvious how to get either of them to work easily with NSOpenPanel.
I decided to take a (very) similar approach to the existing implementations, but to use Core Animation to do the flipping for me and to tweak the setup and tear down to work better with the modal system panels. The key idea I took away from the previous implementations was creating an over-sized, non-opaque window in which a screen shot of the window being flipped would be animated.
The code for creating an over-sized non-opaque window is very straightforward:
NSRect overSizedBounds = NSInsetRect([windowToFlip frame], -100, -100); _flipWindow = [[NSWindow alloc] initWithContentRect:overSizedBounds styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; [_flipWindow setOpaque:NO]; [_flipWindow setHasShadow:NO];
Taking a screen shot of the existing window also turns out to be pretty easy:
NSView* view = [[[self window] contentView] superview];
NSRect bounds = [view bounds];
NSBitmapImageRep* bitmap =
[view bitmapImageRepForCachingDisplayInRect:bounds];
[view cacheDisplayInRect:bounds
toBitmapImageRep:bitmap];
Displaying the screen shot in the over-sized window using Core Animation is neat and clean. I used setWantsLayer to request the content view of my over-sized window be layer backed, and then created a new CALayer and added it as a sub-layer to the over-sized window.
The nested CALayer avoids having to mess with layer backing the NSView, which really doesn’t like to be messed much with. There’s probably some room for potential improvement in moving away from a layer backed view. The sub-layer holds the screen shot of the window being flipped, which thanks to the contents property of CALayer and the [NSBitmapImageRep CGImage] convenience method is also dead simple to implement:
NSView* flipView = [[NSView alloc] initWithFrame:bounds]; [_flipWindow setContentView:flipView]; [flipView setAutoresizingMask: (NSViewWidthSizable|NSViewHeightSizable)]; [flipView setWantsLayer:YES]; _flipLayer = [CALayer layer]; [[flipView layer] addSublayer:_flipLayer]; [_flipLayer setFrame:bounds]; [_flipLayer setContents:(id)[bitmap CGImage]]; [_flipLayer setContentsGravity:kCAGravityCenter];
The flipping code is just a CABasicAnimation that animates transform.animation.y from zero to PI/2. The only minor gotcha to remember is to set up perspective in the transform matrix
CATransform3D transform = CATransform3DIdentity; transform.m34 = 1.0 / -850; [_flipLayer setTransform:transform];
What I spent most of my time on was working to eliminate flicker. Initially, I assumed I could just wrap any transitions between the “real” windows and the “fake” over-sized window with calls to NSDisableScreenUpdates() and NSEnableScreenUpdates().
While those calls were definitely a big piece of the puzzle, I also had to be careful to force the initial display of the window ([NSWindow display]) to occur while updates were disabled. I also had to pay attention to the relative window levels as I was dealing with modal sheets. Both of these ideas are present in the two example programs cited earlier, but the modal sheets, timing and granularity of the delegates from NSOpenPanel and CABasicAnimation took some tweaking.
The last problem I encountered in replicating the silky smooth flip of Apple’s Keynote start-up template screen was some hitching around the point of transition due to setup and tear down cost. Again the solution was present in the example programs cited earlier, but I had to futz around with it a little based on Core Animation and modal timings and delegates.
In short, the solution was to use NSPanel's setAlphaValue: to hide the latency of window creation, screen shot creation and the setting of the contents of the CALayer. After the flip is completed from panel “A” to panel “B”, and panel “B” is presented to the user I perform the setup of the overlay window in anticipation of the next flip. This means I take a screen shot of panel “A” (without actually displaying it to the screen) and set the over-sized window up (also without displaying it to the screen), but setting it’s alpha value to zero so it remains invisible until it’s needed.
It’s easy to notice a hang in the middle of the transition animation, but it’s much harder to notice that the app is hanging at the end of the flip animation. In the latter case, the hang “hides” in the time it takes to visually parse the newly presented contents.
Big caveat: I’m targeting Leopard only and it’s not immediately obvious to me which aspects of the approach I took would fail on which previous versions of OS X. Both of the previously cited examples are approaches that work on past versions of the OS and older hardware, and seem like a good way to go if that kind of support is needed.
Hmm, now what important feature was I supposed to be implementing instead of working on eye candy? Flippin’ !#@$% modal panels.
