Search code examples
macoscocoacore-animationcalayernsview

Using a view with a superlayer as a layer mask


I have a view that I want to overlay with a transparent black layer whose edges match the view exactly. The view does not clip its bounds, so subviews may be hanging off.

The obvious solution is the mask property of CALayer, but the docs say that the mask layer "must not have a superlayer" or the behavior is undefined.

I was hoping that using the presentationLayer of that view would be an effective workaround, but I don't think I fully understand what presentation layers are, as that property returns nil.

Does anyone have tips on how I could mask my transparent black layer to match the shape of the view over which it will be drawn? Thanks.


Solution

  • There’s a less obvious solution to this that should work better than masking: a blend-mode filter, specifically source-atop. If you add your transparent layer as a sibling of the layers it’s supposed to dim, and assign it the appropriate CI compositing filter, it will dim those layers without affecting anything outside of their boundaries. As a bonus, you’ll get correct antialiasing at the edges, unlike the masked-overlay approach.

    Here’s an example. For illustrative purposes I made the dimming layer only half the height of the area it covers, but you’d of course want to make it larger.

    green and purple squares on a blue background, with a central region of the two squares dimmed

    let container = CALayer()
    container.backgroundColor = NSColor.systemBlue.cgColor
    container.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
    
    let dimmedRoot = CALayer()
    let dimmedLayer1 = CALayer()
    dimmedLayer1.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
    dimmedLayer1.backgroundColor = NSColor.systemGreen.cgColor
    dimmedLayer1.transform = CATransform3DMakeRotation(0.3, 0, 0, 1)
    
    let dimmedLayer2 = CALayer()
    dimmedLayer2.frame = CGRect(x: 80, y: 80, width: 100, height: 100)
    dimmedLayer2.backgroundColor = NSColor.systemPurple.cgColor
    dimmedLayer2.transform = CATransform3DMakeRotation(-0.1, 0, 0, 1)
    
    let dimmingLayer = CALayer()
    dimmingLayer.frame = CGRect(x: 0, y: 50, width: 200, height: 100)
    dimmingLayer.backgroundColor = NSColor(white: 0, alpha: 0.5).cgColor
    dimmingLayer.compositingFilter = CIFilter(name: "CISourceAtopCompositing")
    
    dimmedRoot.sublayers = [ dimmedLayer1, dimmedLayer2, dimmingLayer ]
    
    container.addSublayer(dimmedRoot)