2

I have the following code which draws a shape:

let screenSize: CGRect = UIScreen.main.bounds
let cardLayer = CAShapeLayer()
let cardWidth = 350.0
let cardHeight = 225.0
let cardXlocation = (Double(screenSize.width) - cardWidth) / 2
let cardYlocation = (Double(screenSize.height) / 2) - (cardHeight / 2) - (Double(screenSize.height) * 0.05)
cardLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: cardWidth, height: 225.0), cornerRadius: 10.0).cgPath
cardLayer.position = CGPoint(x: cardXlocation, y: cardYlocation)
cardLayer.strokeColor = UIColor.white.cgColor
cardLayer.fillColor = UIColor.clear.cgColor
cardLayer.lineWidth = 4.0        
self.previewLayer.insertSublayer(cardLayer, above: self.previewLayer)

I want everything outside of the shape to be black with an opacity of 50%. That way you can see the camera view still behind it, but it's dimmed, except where then shape is.

I tried adding a mask to previewLayer.mask but that didn't give me the effect I was looking for.

Ethan Allen
  • 14,425
  • 24
  • 101
  • 194

2 Answers2

2

Your impulse to use a mask is correct, but let's think about what needs to be masked. You are doing three things:

  • Dimming the whole thing. Let's call that the dimming layer. It needs a dark semi-transparent background.

  • Drawing the white rounded rect. That's the shape layer.

  • Making a hole in the entire thing. That's the mask.

Now, the first two layers can be the same layer. That leaves only the mask. This is not trivial to construct: a mask affects its owner in terms entirely of its transparency, so we need a mask that is opaque except for an area shaped like the shape of the shape layer, which needs to be clear. To get that, we start with the shape and clip to that shape as we fill the mask — or we can clip to that shape as we erase the mask, which is the approach I prefer.

In addition, your code has some major flaws, the most important of which is that your shape layer has no size. Without a size, there is nothing to mask.

So here, with corrections and additions, is your code; I made this the entirety of a view controller, for testing purposes, and what I'm covering is the entire view controller's view rather than a particular subview or sublayer:

override func viewDidLoad() {
    super.viewDidLoad()
    self.view.backgroundColor = .red
}
private var didInitialLayout = false
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    if didInitialLayout {
        return
    }
    didInitialLayout = true
    let screenSize = UIScreen.main.bounds
    let cardLayer = CAShapeLayer()
    cardLayer.frame = self.view.bounds
    self.view.layer.addSublayer(cardLayer)

    let cardWidth = 350.0 as CGFloat
    let cardHeight = 225.0 as CGFloat
    let cardXlocation = (screenSize.width - cardWidth) / 2
    let cardYlocation = (screenSize.height / 2) - (cardHeight / 2) - (screenSize.height * 0.05)
    let path = UIBezierPath(roundedRect: CGRect(
            x: cardXlocation, y: cardYlocation, width: cardWidth, height: cardHeight),
            cornerRadius: 10.0)
    cardLayer.path = path.cgPath
    cardLayer.strokeColor = UIColor.white.cgColor
    cardLayer.lineWidth = 8.0
    cardLayer.backgroundColor = UIColor.black.withAlphaComponent(0.5).cgColor

    let mask = CALayer()
    mask.frame = cardLayer.bounds
    cardLayer.mask = mask

    let r = UIGraphicsImageRenderer(size: mask.bounds.size)
    let im = r.image { ctx in
        UIColor.black.setFill()
        ctx.fill(mask.bounds)
        path.addClip()
        ctx.cgContext.clear(mask.bounds)
    }
    mask.contents = im.cgImage
}

And here's what we get. I didn't have a preview layer but the background is red, and as you see, the red shows through inside the white shape, which is just the effect you are looking for.

enter image description here

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Wow, amazing answer! I tested the code in my project and it works perfectly! I was setting the frame of `self.previewLayer` elsewhere in the code, and I think the `CAShapeLayer` was inheriting that when I was inserting it, but thank you for the correction. Drawing isn't something I do regularly in my projects, so I really appreciate the help here. – Ethan Allen Mar 22 '21 at 07:07
  • 1
    Layers don’t inherit size from their superlayer. It’s actually more of an issue than you might suppose, because there is no autolayout for independent layers. This code may need modification if the interface can rotate or be resized in any other way. Have fun! – matt Mar 22 '21 at 07:21
0

The shape layer can only affect what it covers, not the space it doesn't cover. Make a path that covers the entire video and has a hole in it where the card should be.

let areaToDarken = previewLayer.bounds // assumes origin at 0, 0
let areaToLeaveClear = areaToDarken.insetBy(dx: 50, dy: 200)
let shapeLayer = CAShapeLayer()
let path = CGPathCreateMutable()
path.addRect(areaToDarken, ...)
path.addRoundedRect(areaToLeaveClear, ...)
cardLayer.frame = previewLayer.bounds // use frame if shapeLayer is sibling
cardLayer.path = path
cardLayer.fillRule = .evenOdd // allow holes
cardLayer.fillColor = black, 50% opacity
drawnonward
  • 53,459
  • 16
  • 107
  • 112