Reputation: 2028
I am trying to create a custom NSView
that hosts a CALayer
hierarchy to perform efficient display. This NSView
is then embedded within a NSTableCellView
that is displayed by a View-Based NSOutlineView
.
The problem is that whenever I expand or collapse an item, all rows are being moved, but the layer's content remains displayed at the position it was before changing the outline.
Scrolling the NSOutlineView
seems to refresh the layers and they resync with their rows at that point.
I have debugged this behavior using Instruments and it seems that the scrolling provokes a layout operation which updates the layers with a setPosition:
call that should have occured when expanding or collapsing items.
Here is some sample code for a simple layer hosting NSView
subclass.
@interface TestView : NSView
@end
@implementation TestView
- (instancetype)initWithFrame:(NSRect)frameRect
{
self = [super initWithFrame:frameRect];
CAShapeLayer* layer = [CAShapeLayer layer];
layer.bounds = self.bounds;
layer.position = CGPointMake(NSMidX(self.bounds), NSMidY(self.bounds));
layer.path = [NSBezierPath bezierPathWithOvalInRect:self.bounds].CGPath;
layer.fillColor = [NSColor redColor].CGColor;
layer.delegate = self;
self.layer = layer;
self.wantsLayer = YES;
return self;
}
@end
I have tried a lot of potential solutions to this problem but I couldn't find any interesting method that gets called on the NSView
instance that could be overriden to call [self.layer setNeedsDisplay]
or [self.layer setNeedsLayout]
. I also tried various setters on the CALayer
itself such as :
layer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
layer.needsDisplayOnBoundsChange = YES;
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay;
Can anyone help me figure out how to make this layer display properly inside a NSOutlineView
?
Upvotes: 15
Views: 623
Reputation: 90621
You shouldn't have to enable layer backing for any of the ancestor views (like the outline view).
In my experience, the layer immediately assigned to a view (as opposed to sublayers) doesn't need its bounds, position, or autoresizing mask to be set. It is automatically made to track the bounds of the view. In fact, I would avoid setting those properties, just in case that breaks the automatic synchronization with the view's bounds rect.
So, the question is: how are you arranging for the view to move or resize with its superview? Are you using auto layout? If so, did you turn off its translatesAutoresizingMaskIntoConstraints
? If yes to both, what constraints are you setting on the view? If no to either, how did you position the view within its superview? What frame did you set? Also, is the superview configured to autoresize its subviews (probably yes, since that's the default)? What is your view's autoresizingMask
?
You could also override -setFrameOrigin:
and -setFrameSize:
in your custom view class and call through to super. Also, add logging to show when that's happening and what the new frame rect is. Is your view being moved as you expect when you expand or collapse rows?
Upvotes: 0
Reputation: 2028
I ended up answering my question. The problem wasn't in the way my TestView was implemented. I simply missed one of the steps for enabling CoreAnimation support within the application. The relevant reference is within the Core Animation Programming Guide.
Basically, in iOS Core Animation and layer-backing is always enabled by default. On OS X, it has to be enabled this way :
Once I enable layer support on any of the NSOutlineView's parents, the various glitches are solved.
Upvotes: 5
Reputation: 5665
It is difficult to read the NSOutlineView
reference documents and find the information about cell reuse that is likely giving you fits here.
You may have looked at outlineViewItemDidCollapse:
but it's kind of a useless for our issue, because it doesn't have a pointer to an NSView
, and that's because it's older than view-based outline views.
Perhaps the one helpful mention, buried within the NSOutlineViewDelegate
protocol, down in the section on view-based NSOutlineView methods, there is a single mention within outlineView:didRemoveRowView:forRow:
that:
The removed rowView may be reused by the table, so any additionally inserted views should be removed at this point.
In other words, when you call the outline view's makeViewWithIdentifier:owner:
, for a cellView or rowView with a particular ID you often get a recycled view. Especially often because of collapse. Incidentally, that method is from the NSTableView
superclass, and in that reference, there's also this comment:
This method may also return a reused view with the same identifier that is no longer available on screen. If a view with the specified identifier can’t be instantiated from the nib file or found in the reuse queue, this method returns nil.
So you have the option of altering the view hierarchy or niling properties in didRemoveRowView:forRow
. However, buried within a third cocoa reference, that for NSView
, there is within the commentary on prepareForReuse
, this comment:
This method offers a way to reset a view to some initial state so that it can be reused. For example, the NSTableView class uses it to prepare views for reuse and thereby avoid the expense of creating new views as they scroll into view. If you implement a view-reuse system in your own code, you can call this method from your own code prior to reusing them.
So, TL;DR, you need to implement prepareForReuse
.
The pertinent references are (mostly) the superclasses of both NSOutlineView
and NSTableCellView
.
And, FWIW, there was a similar question here, where the questioner seems to indicate things are even worse than I think, in that NSOutlineView is more creative behind the scenes than NSTableView.
In my own work with outline views and embedded NSTextViews, I've seen wildly terrible rendering hiccups relating to expand/collapse/scroll that I seem to have managed in just the NSOutlineViewDelegate methods. On iOS they did everyone the favor of renaming makeViewWithIdentifier
to the more explicit dequeueReusableCellViewWithIdentifier
.
Upvotes: 0