Layer backed NSView with custom CALayer not calling updateLayer?

后端 未结 3 2243
别那么骄傲
别那么骄傲 2021-02-13 14:29

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

3条回答
  •  借酒劲吻你
    2021-02-13 15:04

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

提交回复
热议问题