Combine overlapping UIBezierPaths to create a single mask - ios

I'm looking to create a mask in my view that is shaped like several circles. Here blurView is the view I want to have cut outs in, and frames contains CGRect frame values for these circles/ovals.
let holes = UIBezierPath()
for f in frames {
let p = UIBezierPath(ovalInRect: f)
holes.appendPath(p)
}
let path = UIBezierPath(rect: blurView.bounds)
path.appendPath(holes)
path.usesEvenOddFillRule = true
let mask = CAShapeLayer()
mask.path = path.CGPath
mask.fillRule = kCAFillRuleEvenOdd
blurView.layer.mask = mask
This works quite well, except when circles overlap, the contents of the blurView show through instead of the view below it. How can I fix that?
Edit: Using Rob's answer, I got somewhere, but not quite where I want to be. The edges of the mask are ragged, they lack anti-aliasing:
Any way to fix that?

The basic idea is to fill a bitmap context with an opaque colour and then clear anything we want to clip. Grab an image of that bitmap context and use as a mask.
You can copy this into a playground if you like:
import UIKit
// A couple of overlapping circles to play with
let frames = [
CGRect(x: 100, y: 100, width: 200, height: 200),
CGRect(x: 50, y: 50, width: 200, height: 200)
]
let blurView = UIImageView(image: ...) // <== You can drag an image here. Literals are cool.
let size = blurView.bounds.size
let scale = UIScreen.mainScreen().scale
// First, create a bitmap context to work in.
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedFirst.rawValue | CGBitmapInfo.ByteOrder32Little.rawValue)
let ctx = CGBitmapContextCreate(nil, Int(size.width*scale), Int(size.height*scale),
8, 4 * Int(size.width * scale), CGColorSpaceCreateDeviceRGB(), bitmapInfo.rawValue)
CGContextScaleCTM(ctx, scale, scale)
// iOS draws upsidedown from Core Graphics, so you'll probably want to flip your context:
CGContextConcatCTM(ctx, CGAffineTransformMake(1, 0, 0, -1, 0, size.height))
// Everything we want to draw should be opaque
// Everything we want to clip should be translucent
CGContextSetGrayFillColor(ctx, 1, 1) // white
CGContextFillRect(ctx, blurView.bounds)
// And now we clear all our circles
CGContextSetBlendMode(ctx, .Clear)
CGContextSetGrayFillColor(ctx, 1, 0) // clear
for f in frames {
CGContextFillEllipseInRect(ctx, f)
}
// Make the masking image
let maskImage = CGBitmapContextCreateImage(ctx)
// For your playground amusement (you can quicklook this line)
UIImage(CGImage: maskImage)
// Create the masking layer
let mask = CALayer()
mask.frame = blurView.layer.frame
mask.contents = maskImage
// And apply it
blurView.layer.mask = mask
// And the final so you can quicklook it
blurView

Related

How do I make everything outside of a CAShapeLayer black with an opacity of 50% with Swift?

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

SceneKit CALayer unsharp drawing

I'm trying to draw UIBezierPaths and display them on a SCNPlane, though I'm getting a very unsharp result (See in the image) How could I make it sharper?
let displayLayer = CAShapeLayer()
displayLayer.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
displayLayer.path = path.cgPath
displayLayer.fillColor = UIColor.clear.cgColor
displayLayer.strokeColor = stroke.cgColor
displayLayer.lineWidth = lineWidth
let plane = SCNPlane(width:size.width, height: size.height)
let material = SCNMaterial()
material.diffuse.contents = displayLayer
plane.materials = [material]
let node = SCNNode(geometry: plane)
I've tried to increase the displayLayer.frame's size but it only made things smaller.
Thanks for your answer!
Andras
This has been driving me crazy, so hopefully this answer helps someone else who stumbles across this. I'm mapping a CALayer to a SCNMaterial texture for use in an ARKit scene, and everything was blurry/pixelated. Setting the contentsScale property of the CALayer to UIScreen.main.scale didn't do anything, nor playing with shouldRasterize and rasterizationScale.
The solution is to set the layer's frame to the desired frame, multiplied by the screen's scale, otherwise the material is scale transforming the layer to fit the texture. In other words, the layer's size needs to be 3x its desired size.
let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 500 * UIScreen.main.scale, height: 750 * UIScreen.main.scale)
Try to change UIBezierPath "flatness" property (0.1, 0.01, 0.001 etc).

Change color of kCGImageAlphaOnly rendered CGImage

I'm trying to take some huge 32bit PNGs that are actually just black with an alpha channel and present them in an iOS app in a memory-friendly way.
To do that I've tried to re-render the images in an "alpha-only" CGContext:
extension UIImage {
func toLayer() -> CALayer? {
let cgImage = self.cgImage!
let height = Int(self.size.height)
let width = Int(self.size.width)
let colorSpace = CGColorSpaceCreateDeviceGray()
let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width, space: colorSpace, bitmapInfo: CGImageAlphaInfo.alphaOnly.rawValue)!
context.draw(cgImage, in: CGRect(origin: .zero, size: self.size))
let image = context.makeImage()!
let layer = CALayer()
layer.contents = image
layer.contentsScale = self.scale
return layer
}
}
This is awesome! It takes memory usage down from 180MB to about 18MB, which is actually better than I expected.
The issue is, the black (or, now, opaque) parts of the image are no longer black, but are white instead.
It seems like it should be an easy fix to change the coloration of the opaque bits but I can't find any information about it online. Do you have an idea?
I've managed to answer my own question. By setting the alpha-only image as the contents of the output layer's mask, we can set the background colour of the layer to anything we want (including non-greyscale values), and still keep the memory benefits!
I've included the final code because I'm surely not the only one interested in this method:
extension UIImage {
func to8BitLayer(color: UIColor = .black) -> CALayer? {
guard let cgImage = self.cgImage else { return nil }
let height = Int(self.size.height * scale)
let width = Int(self.size.width * scale)
let colorSpace = CGColorSpaceCreateDeviceGray()
guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width, space: colorSpace, bitmapInfo: CGImageAlphaInfo.alphaOnly.rawValue) else {
print("Couldn't create CGContext")
return nil
}
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
guard let image = context.makeImage() else {
print("Couldn't create image from context")
return nil
}
// Note that self.size corresponds to the non-scaled (retina) dimensions, so is not the same size as the context
let frame = CGRect(origin: .zero, size: self.size)
let mask = CALayer()
mask.contents = image
mask.contentsScale = scale
mask.frame = frame
let layer = CALayer()
layer.backgroundColor = color.cgColor
layer.mask = mask
layer.contentsScale = scale
layer.frame = frame
return layer
}
}

Cut transparent hole in the shape of UIImage into UIView

I have an UIView in which I want to create a hole in the shape of the content of a UIImageView, but I can't get it to work. I can, however, mask an UIView to the shape of the UIImage.
My code:
let doorsMaskBounds = CGRect(x: 0, y: 0, width: 111, height: 111)
// Background view
let doorsBgView = UIView(frame: doorsMaskBounds)
doorsBgView.center = navigationController.view.center
doorsBgView.backgroundColor = UIColor.yellow
vc.view.addSubview(doorsBgView)
vc.view.bringSubview(toFront: doorsBgView)
// Logo Mask
doorsBgView.layer.mask = CALayer()
doorsBgView.layer.mask?.contents = UIImage(named: "doors")!.cgImage
doorsBgView.layer.mask?.bounds = doorsMaskBounds
doorsBgView.layer.mask?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
doorsBgView.layer.mask?.position = CGPoint(x: doorsBgView.frame.width / 2, y: doorsBgView.frame.height / 2)
which results into:
I know how to "cut" a hole into a UIView in form of a shape I draw myself, as explained here, I just can't seem to figure out how the get the shape of the UIImage and apply it as the cut.

Circle image crop when using UIImagePickerController Swift

I need to crop images circle when importing from photo library, just like in the stock contacts app. I found a few solutions but all of them were in Objective-C. I found them hard to translate. Does anyone know a useful swift library or so?
this works for me
profileImage.image = UIImageVar
profileImage.layer.borderWidth = 1
profileImage.layer.borderColor = UIColor.blackColor().CGColor
profileImage.layer.cornerRadius = profileImage.frame.height/2
profileImage.clipsToBounds = true
If you're seeking something like Core Graphics, you can take the following for references.
let ctx = UIGraphicsGetCurrentContext()
let image = UIImage(named: "YourImageName")!
// Oval Drawing
let width = image.size.width
let height = image.size.height
let ovalPath = UIBezierPath(ovalInRect: CGRectMake(0, 0, width, height))
CGContextSaveGState(ctx)
ovalPath.addClip()
image.drawInRect(CGRectMake(0, 0, width, height))
CGContextRestoreGState(ctx)

Resources