9
votes

I have a layer-hosting view set up like this in a custom NSView subclass:

[self setLayer:rootLayer];
[self setWantsLayer:YES];

I add all the sublayers to the layer tree after I called setNeedsDisplay on each sublayer. Each layer's content is provided by a drawLayer:inContext method of my layer's delegate.

Here is my problem:

After initializing my view the view gets draw correctly. However, when the model has changed and I call [myCustomView setNeedsDisplay:YES]; from my view controller the drawLayer:inContext is not called.

I am confused now how to update the view:

  • Do I have to call the setNeedsDisplay method on each CALayer in the layer tree?
  • Should not the call of setNeedsDisplay:YES on the layer-hosting view itself trigger the redraw of the whole layer tree?

Thanks for your help.

Edit

I have found something in the NSView Class reference

A layer-backed view is a view that is backed by a Core Animation layer. Any drawing done by the view is the cached in the backing layer. You configured a layer-backed view by simply invoking setWantsLayer: with a value of YES. The view class will automatically create the a backing layer for you, and you use the view class’s drawing mechanisms. When using layer-backed views you should never interact directly with the layer.

A layer-hosting view is a view that contains a Core Animation layer that you intend to manipulate directly. You create a layer-hosting view by instantiating an instance of a Core Animation layer class and setting that layer using the view’s setLayer: method. After doing so, you then invoke setWantsLayer: with a value of YES. When using a layer-hosting view you should not rely on the view for drawing, nor should you add subviews to the layer-hosting view.

link to documentation

In my case I have a layer-hosting view. So does that indeed mean that I have to trigger the redraw manually? Should I implement a pseudo drawRect method in the custom NSView to call the appropriate setNeedsDisplay on the CALayers that changed?

3

3 Answers

10
votes

After further research in Apple's sample code of a kiosk-style menu I found out that if you are using a layer-hosting view, you have to take care of the screen updates which are neccessary due to model changes yourself. Calling setNeedsDisplay:YES on the NSView will not do anything.

So what one has to do if one has to update a view one should write a method like reloadData and in it one should call setNeedsDisplayon each CALayer that needs a refresh. I am still not sure if a call to this method on the root layer will propagate through all the children layers but I do not think so.

I solved the problem now by calling setNeedsDisplay on the individual CALayers that needed recaching. It works without problems.

2
votes

There is also an oft-used practice of having an empty "drawrect", a la -(void) drawRect:(NSRect)dirtyRect {} to help coerce things into drawing, i believe via good ole view.needsDisplay = YES;.

and it should be noted.. that what is indeed happening is that - by saying your NSView *view; is layer.delegate = view; causes the layer to be drawn when [layer setNeedsDisplay]; is called.... via - (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {...}..

along the same vein... when saying layer.layoutManager = view... subsequent demands that [layer setNeedsLayout]; will be fulfilled only when the - (void) layoutSublayersOfLayer:(CALayer *)layer {..} method is implemented..

These vital concepts are glossed over / strewn about in Apple's docs... and they are really so pivotal to making absolutely anything work at all.

0
votes

You can automatically delegate the setNeedsDispay: by changing the redraw policy of the view. You have to assign NSViewLayerContentsRedrawOnSetNeedsDisplay to the property layerContentsRedrawPolicy (see https://developer.apple.com/documentation/appkit/nsview/1483514-layercontentsredrawpolicy). This will trigger redrawing of the layer when you send setNeedsDisplay: to the view:

[self setLayer:rootLayer];
[self setWantsLayer:YES];
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay;

or in Swift:

layer = rootLayer
wantsLayer = true
layerContentsRedrawPolicy = .onSetNeedsDisplay