I have a custom layer-backed NSView and have overidden the makeBackingLayer method to return a custom CALayer subclass. I have also overriden wantsUpdateLayer to return true thereby fully opting into layer-like drawing.
override func makeBackingLayer() -> CALayer {
return Layer() // my custom class with "drawLayer:" implemented
}
override var wantsUpdateLayer:Bool{
return true
}
// never called
override func updateLayer() {
super.updateLayer()
println("updateLayer after")
self.layer?.borderWidth += 1
}
Once I do this, I find that when I set NSView.needsDisplay = true
it routes calls to the custom layer's drawInContext: method as opposed to the updateLayer: method. Why does it do this? In my example, I have checked that if I remove the makeBackingLayer
override then my updateLayer
is called in the expected manner.
I can't quite put my finger on it, but other instances point to the notion that when your makeBackingLayerreturns a custom
CALayer` that you actually have your custom layer hosted inside a parent backing layer. (Pure Speculation on my part)
Further, would there be different performance characteristics between the two drawing routes given that CALayer's drawInContext: is more "low-level"? See this SO question for more detail on that question: Layer-backed NSView performance with rendering directly in CALayer.drawInContext:
Any insight would be greatly appreciated.
When you override makeBackingLayer
, it becomes your responsibility to set up that layer, including setting its delegate: CALayerDelegate
. It's enough to set the delegate
to the view using that layer. From there, you can implement any of those delegate methods, though you probably want func display(_ layer: CALayer)
. See Core Animation Guide - Providing a Layer's Contents for more on this.
The code path for updateLayer
depends on the NSView
using the default layer type. Here is my working NSView
subclass that redraws on bounds change:
/**
A view backed by a CAShapeLayer that can inscribe an image inside the
shape layer's circular path
*/
@IBDesignable final class InscribedImageView: NSView, CALayerDelegate {
@IBInspectable private var image: NSImage?
@IBInspectable private var color: NSColor = .clear
// swiftlint:disable:next force_cast
private var shapeLayer: CAShapeLayer { return self.layer as! CAShapeLayer }
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
configureViewSetting()
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
configureViewSetting()
}
private func configureViewSetting() {
wantsLayer = true
}
override func makeBackingLayer() -> CALayer {
let layer = CAShapeLayer()
layer.delegate = self
layer.needsDisplayOnBoundsChange = true
return layer
}
func display(_ layer: CALayer) {
inscribe(image, in: color)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
inscribe(image, in: color)
}
func inscribe(_ image: NSImage?, in color: NSColor) {
self.color = color
defineCircle(color: color)
subviews.forEach { $0.removeFromSuperview() }
if let image = image {
self.image = image
let imageView = NSImageView(image: image)
imageView.imageScaling = .scaleProportionallyUpOrDown
imageView.frame = bounds
addSubview(imageView)
}
}
private func defineCircle(color: NSColor) {
shapeLayer.path = CGPath(ellipseIn: shapeLayer.bounds, transform: nil)
shapeLayer.fillColor = color.cgColor
shapeLayer.strokeColor = color.cgColor
}
}
Note that I didn't need to set wantsUpdateLayer
to return true on my subclass OR the subclass's layerContentsRedrawPolicy = LayerContentsRedrawPolicy.duringViewResize
. Presumably this is because both rely on the stock layer type, not a subclass.
Here are some more useful resources from my previous, incorrect answer:
Apple's Core Animation Guide
Overriding wantsUpdateLayer and returning YES causes the NSView class to follow an alternate rendering path. Instead of calling drawRect:, the view calls your updateLayer method, the implementation of which must assign a bitmap directly to the layer’s contents property. This is the one scenario where AppKit expects you to set the contents of a view’s layer object directly.
Also in NSView.h, the docs for updateLayer
are
/* Layer Backed Views: If the view responds YES to wantsUpdateLayer, then updateLayer will be called as opposed to drawRect:. This method should be used for better performance; it is faster to directly set the layer.contents with a shared image and inform it how to stretch with the layer.contentsCenter property instead of drawing into a context with drawRect:. In general, one should also set the layerContentsRedrawPolicy to an appropriate value in the init method (frequently NSViewLayerContentsRedrawOnSetNeedsDisplay is desired). To signal a refresh of the layer contents, one will then call [view setNeedsDisplay:YES], and -updateLayer will be lazily called when the layer needs its contents. One should not alter geometry or add/remove subviews (or layers) during this method. To add subviews (or layers) use -layout. -layout will still be called even if autolayout is not enabled, and wantsUpdateLayer returns YES. */
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With