Is there a proper way to handle overlapping NSView siblings?

I'm working on a Cocoa application, and I've run into a situation where I would like to have two NSView objects overlap. I have a parent NSView which contains two subviews (NSView A and NSView B), each of which can have several subviews of their own.

Is there a proper way to handle this kind of overlap? NSView B would always be "above" NSView A, so I want the overlapped portions of NSView A to be masked out.


Solution 1:

Chris, the only solution is to use CALayers. That is definitely the one and only solution.

NSViews are plain broken in OSX (Sept 2010): siblings don't work properly. One or the other will randomly appear on top.

Just to repeat, the problem is with siblings.

To test this: using NSViews and/or nsimageviews. Make an app with a view that is one large image (1000x1000 say). In the view, put three or four small images/NSViews here and there. Now put another large 1000x1000 image on top. Build and launch the app repeatedly - you'll see it is plain broken. Often the underneath (small) layers will appear on top of the large covering layer. if you turn on layer-backing on the NSViews, it does not help, no matter what combo you try. So that's the definitive test.

You have to abandon NSViews and use CALayers and that's that.

The only annoyance with CALayers is that you can't use the IB to set up your stuff. You have to set all the layer positions in code,

yy = [CALayer layer];
yy.frame = CGRectMake(300,300, 300,300);

Make one NSView only, who's only purpose is to hold your first CALayer (perhaps called 'rear'), and then just put all your CALayers inside rear.

rear = [CALayer layer];
rear.backgroundColor = CGColorCreateGenericRGB( 0.75, 0.75, 0.75, 1.0 );

[yourOnlyNsView setLayer:rear]; // these two lines must be in this order
[yourOnlyNsView setWantsLayer:YES]; // these two lines must be in this order

[rear addSublayer:rr];
[rear addSublayer:yy];
     [yy addSublayer:s1];
     [yy addSublayer:s2];
     [yy addSublayer:s3];
     [yy addSublayer:s4];
[rear addSublayer:tt];
[rear addSublayer:ff];

everything then works utterly perfectly, you can nest and group anything you want and it all works flawlessly with everything properly appearing above/below everything it should appear above/below, no matter how complex your structure. Later you can do anything to the layers, or shuffle things around in the typcial manner,

-(void) shuff
{
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:0.0f]
              forKey:kCATransactionAnimationDuration];
if ..
    [rear insertSublayer:ff below:yy];
else
    [rear insertSublayer:ff above:yy];
[CATransaction commit];
}

(The only reason for the annoying 'in zero seconds' wrapper for everything you do, is to prevent the animation which is given to you for free - unless you want the animation!)

By the way in this quote from Apple,

For performance reasons, Cocoa does not enforce clipping among sibling views or guarantee correct invalidation and drawing behavior when sibling views overlap.

Their following sentence ...

If you want a view to be drawn in front of another view, you should make the front view a subview (or descendant) of the rear view.

Is largely nonsensical (you can't necessarily replace siblings with subs; and the obvious bug described in the test above still exists).

So it's CALayers! Enjoy!

Solution 2:

If your application is 10.5-only, turn on layers for the views and it should just work.

If you're meaning to support 10.4 and below, you'll need to find a way not to have the views overlap, because overlapping sibling views is undefined behavior. As the View Programming Guide says:

For performance reasons, Cocoa does not enforce clipping among sibling views or guarantee correct invalidation and drawing behavior when sibling views overlap. If you want a view to be drawn in front of another view, you should make the front view a subview (or descendant) of the rear view.

I've seen some hacks that can make it kinda work sometimes, but it's not anything you can rely on. You'll need to either make View A a subview of View B or make one giant view that handles both of their duties.

Solution 3:

There is a way to do it without using CALayers and an App I have been working on can prove it. Create two windows and use this:

[mainWindow addChildWindow:otherWindow ordered:NSWindowAbove];

To remove "otherWindow" use:

[mainWindow removeChildWindow:otherWindow];

[otherWindow orderOut:nil];

And you will probably want to take the window's title bar with:

[otherWindow setStyleMask:NSBorderlessWindowMask];

Solution 4:

In order to ensure that NSView B always overlaps NSView A, make sure that you use the correct NSWindowOrderingMode when you are adding the subview:

[parentView addSubview:B positioned:NSWindowAbove relativeTo:A];

You should also keep in mind that the hidden portions of A will not be asked to redraw if view B is 100% opaque.

If you are moving the subviews, you also need to make sure that you call -setNeedsDisplayInRect: for for the areas of the view you are uncovering.