How to cut portion of CALayer in iOS Swift? [duplicate] - ios

This question already has answers here:
How can I 'cut' a transparent hole in a UIImage?
(4 answers)
Closed 3 years ago.
I am trying to create a QR Reader. For that I am showing a rectOfInterest with some CALayer for visual representation. I want to show a box with some border at the corners and black background with some opacity so hide the other view from the AVCaptureVideoPreviewLayer. What I have achieved till now looks like this:
As you can see the CALayer is there but I want to cut the box portion of the layer so that that blackish thing does not come there. The code I am using to do this is like below:
func createTransparentLayer()->CALayer{
let shape = CALayer()
shape.frame = self.scanView.layer.bounds
shape.backgroundColor = UIColor.black.cgColor
shape.opacity = 0.7
return shape
}
I looked into other questions for this, seems like you have the mask the layer with the cut portion. So I subclassed the CALayer and cleared the context in drawInContext and set the mask property of the super layer to this. After that I get nothing. Everything is invisible there. What is wrong in this?
The code I tried is this:
class TransparentLayer: CALayer {
override func draw(in ctx: CGContext) {
self.backgroundColor = UIColor.black.cgColor
self.opacity = 0.7
self.isOpaque = true
ctx.clear(CGRect(x: superlayer!.frame.size.width / 2 - 100, y: superlayer!.frame.size.height / 2 - 100, width: 200, height: 200))
}
}
then set the mask property like this:
override func viewDidLayoutSubviews() {
self.rectOfInterest = CGRect(x: self.scanView.layer.frame.size.width / 2 - 100, y: self.scanView.layer.frame.size.height / 2 - 100, width: 200, height: 200)
scanView.rectOfInterest = self.rectOfInterest
let shapeLayer = self.createFrame()
scanView.doInitialSetup()
self.scanView.layer.mask = self.createTransparentLayer()
self.scanView.layer.addSublayer(shapeLayer)
}
here the shapeLayer is the bordered corner in the screenshot. How can I achieve this?

I have added view in my view controller which is centre align vertically and horizontally. Also fixed height and width to 200. Then created extension of UIView and added following code:
extension UIView {
func strokeBorder() {
self.backgroundColor = .clear
self.clipsToBounds = true
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = UIBezierPath(rect: self.bounds).cgPath
self.layer.mask = maskLayer
let line = NSNumber(value: Float(self.bounds.width / 2))
let borderLayer = CAShapeLayer()
borderLayer.path = maskLayer.path
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = UIColor.white.cgColor
borderLayer.lineDashPattern = [line]
borderLayer.lineDashPhase = self.bounds.width / 4
borderLayer.lineWidth = 10
borderLayer.frame = self.bounds
self.layer.addSublayer(borderLayer)
}
}
Use:
By using outlet of view and call method to set border as you have request.
self.scanView.strokeBorder()
To clear the backgroundColor respective to mask view, I have added following code to clear it.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.scanView.strokeBorder()
self.backgroundView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
// Draw a graphics with a mostly solid alpha channel
// and a square of "clear" alpha in there.
UIGraphicsBeginImageContext(self.backgroundView.bounds.size)
let cgContext = UIGraphicsGetCurrentContext()
cgContext?.setFillColor(UIColor.white.cgColor)
cgContext?.fill(self.backgroundView.bounds)
cgContext?.clear(CGRect(x:self.scanView.frame.origin.x, y:self.scanView.frame.origin.y, width: self.scanView.frame.width, height: self.scanView.frame.height))
let maskImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// Set the content of the mask view so that it uses our
// alpha channel image
let maskView = UIView(frame: self.backgroundView.bounds)
maskView.layer.contents = maskImage?.cgImage
self.backgroundView.mask = maskView
}
Output:
I'm not using camera in background.

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

Cut transparent hole in UIView using UIImageView

I'm trying to create the image below for my QR camera view.
I have an image for the camera overlay (which is the square in the middle of the screen).
Ive looked at similar topics on stackoverflow and found out how to cut out the camera overlay image from the black background (0.75% transparent) so that it leaves the empty space in the middle, however I'm having some real issues with its placement, and I cant find out what is behaving so weirdly.
Here is the code that I use to create the background black image and also to cut out the square in the center:
// Create a view filling the screen.
let backgroundView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
// Set a semi-transparent, black background.
backgroundView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75)
// Create the initial layer from the view bounds.
let maskLayer = CAShapeLayer()
maskLayer.frame = backgroundView.bounds
maskLayer.fillColor = UIColor.black.cgColor
// Create the path.
let path = UIBezierPath(rect: backgroundView.bounds)
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
// Append the overlay image to the path so that it is subtracted.
path.append(UIBezierPath(rect: camOverlayImageView.frame))
maskLayer.path = path.cgPath
// Set the mask of the view.
backgroundView.layer.mask = maskLayer
// Add the view so it is visible.
self.view.addSubview(backgroundView)
The camOverlayImageView is created in storyboard and all I'm using on it are constraints to center it both vertically and horizontally to the superview like this:
However, when I do this, this is what I'm getting on the device:
If anyone might know what might be causing this or how to fix it, it would be greatly appreciated as I can't seem to find it.
I can of course manually move the frame and offset it like this but that isn't the correct way to do this:
let overlayFrame = camOverlayImageView.frame
// Append the overlay image to the path so that it is subtracted.
path.append(UIBezierPath(rect: CGRect(
x: overlayFrame.origin.x + 18,
y: overlayFrame.origin.y - 6,
width: overlayFrame.size.width,
height: overlayFrame.size.height))
)
maskLayer.path = path.cgPath
Instead of doing what I was previously doing:
path.append(UIBezierPath(rect: camOverlayImageView.frame))
maskLayer.path = path.cgPath
Most likely...
You are creating your mask too early - before auto-layout has sized / positioned the views.
Try it like this:
class HoleInViewController: UIViewController {
#IBOutlet var camOverlayImageView: UIView!
let backgroundView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = UIColor.black.withAlphaComponent(0.75)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(backgroundView)
NSLayoutConstraint.activate([
backgroundView.topAnchor.constraint(equalTo: view.topAnchor),
backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Create the initial layer from the view bounds.
let maskLayer = CAShapeLayer()
maskLayer.frame = backgroundView.bounds
maskLayer.fillColor = UIColor.black.cgColor
// Create the path.
let path = UIBezierPath(rect: backgroundView.bounds)
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
// Append the overlay image to the path so that it is subtracted.
path.append(UIBezierPath(rect: camOverlayImageView.frame))
maskLayer.path = path.cgPath
// Set the mask of the view.
backgroundView.layer.mask = maskLayer
}
}

Shadows masks to bounds only on the left and upper side. What causes this?

I'm trying to achieve a feature that allows adding a shadow to a transparent button. For this, I'm creating a layer that masks the shadows inside the view. However, my shadows are clipped on the left and upper sides but not clipped on the right and lower sides.
Here is how it looks (This is not a transparent button but they're also working fine except the shadow being clipped like this.):
And here is my code for achieving this:
private func applyShadow() {
layer.masksToBounds = false
if shouldApplyShadow && shadowLayer == nil {
shadowLayer = CAShapeLayer()
let shapePath = CGPath(roundedRect: bounds, cornerWidth: cornerRadi, cornerHeight: cornerRadi, transform: nil)
shadowLayer.path = shapePath
shadowLayer.fillColor = backgroundColor?.cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowRadius = shadowRadius ?? 8
shadowLayer.shadowColor = (shadowColor ?? .black).cgColor
shadowLayer.shadowOffset = shadowOffset ?? CGSize(width: 0, height: 0)
shadowLayer.shadowOpacity = shadowOpacity ?? 0.8
layer.insertSublayer(shadowLayer!, at: 0)
/// If there's background color, there is no need to mask inner shadows.
if backgroundColor != .none && !(innerShadows ?? false) {
let maskLayer = CAShapeLayer()
maskLayer.path = { () -> UIBezierPath in
let path = UIBezierPath()
path.append(UIBezierPath(cgPath: shapePath))
path.append(UIBezierPath(rect: UIScreen.main.bounds))
path.usesEvenOddFillRule = true
return path
}().cgPath
maskLayer.fillRule = .evenOdd
shadowLayer.mask = maskLayer
}
}
}
I think that's something related to the Even-Odd Fill Rule algorithm, I'm not sure. But how can I overcome this clipping problem?
Thanks in advance.
EDIT:
This is what a transparent button with borders and text on it looks when shadow applied.. Which I don't want.
What it should look like this. No shadows inside but also a clear background color. (Except the clipped top and left sides):
I think a couple issues...
You are appending UIBezierPath(rect: UIScreen.main.bounds) to your path, but that puts the top-left corner at the top-left corner of the layer... which "clips" the top-left shadow.
If you DO have a background color, you'll need to clip those corners as well, or they will "bleed" outside the rounded corners.
Give this a try (only slightly modified). It's designated #IBDesignable so you can see how it looks in Storyboard / Interface Builder (I did not set any of the properties to inspectable -- I'll leave that up to you if you want to do so):
#IBDesignable
class MyRSButton: UIButton {
var shouldApplyShadow: Bool = true
var innerShadows: Bool?
var cornerRadi: CGFloat = 8.0
var shadowLayer: CAShapeLayer!
var shadowRadius: CGFloat?
var shadowColor: UIColor?
var shadowOffset: CGSize?
var shadowOpacity: Float?
override func layoutSubviews() {
super.layoutSubviews()
applyShadow()
}
private func applyShadow() {
// needed to prevent background color from bleeding past
// the rounded corners
cornerRadi = bounds.size.height * 0.5
layer.cornerRadius = cornerRadi
layer.masksToBounds = false
if shouldApplyShadow && shadowLayer == nil {
shadowLayer = CAShapeLayer()
let shapePath = CGPath(roundedRect: bounds, cornerWidth: cornerRadi, cornerHeight: cornerRadi, transform: nil)
shadowLayer.path = shapePath
shadowLayer.fillColor = backgroundColor?.cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowRadius = shadowRadius ?? 8
shadowLayer.shadowColor = (shadowColor ?? .black).cgColor
shadowLayer.shadowOffset = shadowOffset ?? CGSize(width: 0, height: 0)
shadowLayer.shadowOpacity = shadowOpacity ?? 0.8
layer.insertSublayer(shadowLayer!, at: 0)
/// If there's background color, there is no need to mask inner shadows.
if backgroundColor != .none && !(innerShadows ?? false) {
let maskLayer = CAShapeLayer()
maskLayer.path = { () -> UIBezierPath in
let path = UIBezierPath()
path.append(UIBezierPath(cgPath: shapePath))
// define a rect that is 80-pts wider and taller
// than the button... this will "expand" it from center
let r = bounds.insetBy(dx: -40, dy: -40)
path.append(UIBezierPath(rect: r))
path.usesEvenOddFillRule = true
return path
}().cgPath
maskLayer.fillRule = .evenOdd
shadowLayer.mask = maskLayer
}
}
}
}
Result:

Change TabBar mask color (non-transparent)

I am creating a TabBar with top left and top right corners rounded.
I'm using a layer mask to achieve this and it works fine, however I need the mask color to be white (its transparent showing the VC background color with the below code).
Is it possible to set the mask background color white with below approach?
I've tried setting layer and layer.mask background colours but with no success (I can't change the VC background color).
current code:
self.tabBar.layer.masksToBounds = true
self.tabBar.isTranslucent = true
self.tabBar.barStyle = .default
self.tabBar.layer.cornerRadius = 28
self.tabBar.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
Thanks.
If you want to set background color to layer mask, you need another layer
Is this the effect you needed?
You may try this:
extension UITabBar {
func roundCorners(corners: UIRectCorner, backgroundColor: UIColor, cornerColor: UIColor, radius: Int = 20) {
self.backgroundColor = cornerColor
let parentLayer = CALayer()
parentLayer.frame = bounds
parentLayer.backgroundColor = backgroundColor.cgColor
layer.insertSublayer(parentLayer, at: 0)
let maskPath = UIBezierPath(roundedRect: bounds,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.frame = bounds
mask.path = maskPath.cgPath
parentLayer.mask = mask
}
}

Simply mask a UIView with a rectangle

I want to know how to simply mask the visible area of a UIView of any kind. All the answers/tutorials I've read so far describe masking with an image, gradient or creating round corners which is way more advanced than what I am after.
Example: I have a UIView with the bounds (0, 0, 100, 100) and I want to cut away the right half of the view using a mask. Therefore my mask frame would be (0, 0, 50, 100).
Any idea how to do this simply? I don't want to override the drawrect method since this should be applicable to any UIView.
I've tried this but it just makes the whole view invisible.
CGRect mask = CGRectMake(0, 0, 50, 100);
UIView *maskView = [[UIView alloc] initWithFrame:mask];
viewToMask.layer.mask = maskView.layer;
Thanks to the link from MSK, this is the way I went with which works well:
// Create a mask layer and the frame to determine what will be visible in the view.
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGRect maskRect = CGRectMake(0, 0, 50, 100);
// Create a path with the rectangle in it.
CGPathRef path = CGPathCreateWithRect(maskRect, NULL);
// Set the path to the mask layer.
maskLayer.path = path;
// Release the path since it's not covered by ARC.
CGPathRelease(path);
// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
Thanks for answers guys.
In case someone can't find suitable answer on SO for this question for hours, like i just did, i've assembled a working gist in Swift 2.2 for masking/clipping UIView with CGRect/UIBezierPath:
https://gist.github.com/Flar49/7e977e81f1d2827f5fcd5c6c6a3c3d94
extension UIView {
func mask(withRect rect: CGRect, inverse: Bool = false) {
let path = UIBezierPath(rect: rect)
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
func mask(withPath path: UIBezierPath, inverse: Bool = false) {
let path = path
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
}
Usage:
let viewSize = targetView.bounds.size
let rect = CGRect(x: 20, y: 20, width: viewSize.width - 20*2, height: viewSize.height - 20*2)
// Cuts rectangle inside view, leaving 20pt borders around
targetView.mask(withRect: rect, inverse: true)
// Cuts 20pt borders around the view, keeping part inside rect intact
targetView.mask(withRect: rect)
Hope it will save someone some time in the future :)
No need any mask at all.
Just put it into a wrapper view with the smaller frame, and set clipsToBounds.
wrapper.clipsToBounds = true
Very simple example in a Swift ViewController, based on the accepted answer:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let redView = UIView(frame: view.bounds)
view.addSubview(redView)
view.backgroundColor = UIColor.green
redView.backgroundColor = UIColor.red
mask(redView, maskRect: CGRect(x: 50, y: 50, width: 50, height: 50))
}
func mask(_ viewToMask: UIView, maskRect: CGRect) {
let maskLayer = CAShapeLayer()
let path = CGPath(rect: maskRect, transform: nil)
maskLayer.path = path
// Set the mask of the view.
viewToMask.layer.mask = maskLayer
}
}
Output
An optional layer whose alpha channel is used as a mask to select
between the layer's background and the result of compositing the
layer's contents with its filtered background.
#property(retain) CALayer *mask
The correct way to do what you want is to create the maskView of the same frame (0, 0, 100, 100) as the viewToMask which layer you want to mask. Then you need to set the clearColor for the path you want to make invisible (this will block the view interaction over the path so be careful with the view hierarchy).
Swift 5 , thanks #Dan Rosenstark
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let red = UIView(frame: view.bounds)
view.addSubview(red)
view.backgroundColor = UIColor.cyan
red.backgroundColor = UIColor.red
red.mask(CGRect(x: 50, y: 50, width: 50, height: 50))
}
}
extension UIView{
func mask(_ rect: CGRect){
let mask = CAShapeLayer()
let path = CGPath(rect: rect, transform: nil)
mask.path = path
// Set the mask of the view.
layer.mask = mask
}
}
Setting MaskToBounds is not enough, for example, in scenario where you have UIImageView that is positioned inside RelativeLayout. In case that you put your UIImageView to be near top edge of the layout and then you rotate your UIImageView, it will go over layout and it won't be cropped. If you set that flag to true, it will be cropped yes, but it will also be cropped if UIImageView is on the center of layout, and that is not what you want.
So, determinating maskRect is the right approach.

Resources