Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Layer backed NSView with custom CALayer not calling updateLayer?

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 customCALayer` 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.

like image 260
Sam Avatar asked Mar 19 '15 09:03

Sam


1 Answers

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

like image 179
nteissler Avatar answered Sep 27 '22 22:09

nteissler