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 the
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. */