Search code examples
macoscalayernsviewmetal

drawRect: Not called for an NSView with a CAMetalLayer


I have a layer-backed (not layer-hosted) subclass of NSView called MetalView:

import Cocoa
import Metal

class MetalView: NSView {

    // MARK: Properties

    var metalLayer: CAMetalLayer {
        return layer as! CAMetalLayer
    }

    // MARK: - Overrides

    override func awakeFromNib() {
        super.awakeFromNib()
        initialize()
    }

    override func drawRect(dirtyRect: NSRect) {
        super.drawRect(dirtyRect)
        render()
    }

    override func drawLayer(layer: CALayer, inContext ctx: CGContext) {
        super.drawLayer(layer, inContext: ctx)
        render()
    }

    override func displayLayer(layer: CALayer) {
        super.displayLayer(layer)
        render()
    }

    override func makeBackingLayer() -> CALayer {
        return CAMetalLayer()
    }

    override var wantsUpdateLayer: Bool {
        return false
    }

    // MARK: - Initialization

    func initialize() {
        // Layer
        wantsLayer = true
        metalLayer.delegate = self
        metalLayer.device = MTLCreateSystemDefaultDevice()
        metalLayer.pixelFormat = .BGRA8Unorm
        metalLayer.framebufferOnly = true
    }

    // MARK: - Rendering

    func render() {
        // Rendering
        metalLayer.frame = frame
        NSLog("rendering")
    }

}

The render method is not getting called. I tried setting wantsUpdateLayer to true. The render method gets called with I remove this part:

wantsLayer = true
metalLayer.delegate = self
metalLayer.device = MTLCreateSystemDefaultDevice()
metalLayer.pixelFormat = .BGRA8Unorm
metalLayer.framebufferOnly = true

Solution

  • From the wantsLayer docs:

    If the wantsUpdateLayer method returns NO, you should not interact with the underlying layer object directly.

    Setting values on metalLayer and modifying its frame count as "interact[ing] with the underlying layer object directly." You're not allowed to do that in this configuration. The layer doesn't belong to you. It belongs to the system. The system is free to create these caching layers any time it wants. It doesn't have to use it and there doesn't have to be just one. (Core Animation often creates extra layers; that's completely normal.)

    If you want to interact with the layer directly, you should set wantsUpdateLayer to be true and then implement your drawing code in updateLayer. You shouldn't try to mix drawRect: with a layer you mess with directly.

    For another version of this problem, see Why is an empty drawRect interfering with a simple animation?