CAShapeLayer cutout in UIView - ios

My goal is to create a pretty opaque background with a clear rounded rect inset
let shape = CGRect(x: 0, y: 0, width: 200, height: 200)
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: shape, cornerRadius: 16).cgPath
maskLayer.fillColor = UIColor.clear.cgColor
maskLayer.fillRule = kCAFillRuleEvenOdd
let background = UIView()
background.backgroundColor = UIColor.black.withAlphaComponent(0.8)
view.addSubview(background)
constrain(background) {
$0.edges == $0.superview!.edges
}
background.layer.addSublayer(maskLayer)
// background.layer.mask = maskLayer
When I uncomment background.layer.mask = maskLayer, the view is totally clear. When I have it commented out, I see the semi-opaque background color but no mask cutout
Any idea what I'm doing wrong here? Thanks!

Here is a playground that I think implements your intended effect (minus .edges and constrain - that appear to be your own extensions).
a) used a red background instead of black with alpha (just to make the effect stand out)
b) set maskLayer.backgroundColor instead of maskLayer.fillColor
Adding the maskLayer as another layer to uiview is superfluous (as indicated by #Ken) but seems to do no harm.
import UIKit
import PlaygroundSupport
let bounds = CGRect(x: 0, y: 0, width: 400, height: 200)
let uiview = UIView(frame: bounds)
uiview.backgroundColor = UIColor.red.withAlphaComponent(0.8)
PlaygroundPage.current.liveView = uiview
let shape = CGRect(x: 100, y: 50, width: 200, height: 100)
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: shape, cornerRadius: 16).cgPath
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.fillRule = kCAFillRuleEvenOdd
uiview.layer.mask = maskLayer
image of clear inset
On the other hand, if you intended a picture frame / colored-envelope-with-transparent-window effect like below, then see gist
image of picture frame

Your mask Layer has no size.
Add this line maskLayer.frame = shape
and you don't need background.layer.addSublayer(maskLayer)

Related

Issue with rounded corners of ImageView with gradient colour

I created a rectangle using following code and now I need to rounded the corners of this rectangle. I set layer.cornerRadius also, can anyone help me?
My code as below,
private func setGradientBorder(_ ivUser:UIImageView) {
ivUser.layer.masksToBounds = true
ivUser.layer.cornerRadius = ivUser.frame.width / 2
let gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPoint.zero, size: ivUser.frame.size)
gradient.colors = [UIColor.blue.cgColor, UIColor.green.cgColor]
let maskPath = UIBezierPath(roundedRect: ivUser.bounds,
byRoundingCorners: [.allCorners],
cornerRadii: CGSize(width: ivUser.frame.width/2, height: ivUser.frame.height/2)).cgPath
let shape = CAShapeLayer()
shape.lineWidth = 2
shape.path = maskPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradient.mask = shape
ivUser.layer.addSublayer(gradient)
}
output: gradient does not display properly rounded
This is how UIBezierPath works. Half of the line width will go on one side of the actual path, and half will go on another. Thats why it looks kinda cut out.
You will need to inset your paths rect by half of the line width, something like this:
let lineWidth: CGFloat = 2
let maskPath = UIBezierPath(roundedRect: ivUser.bounds.insetBy(dx: lineWidth/2, dy: lineWidth/2),
byRoundingCorners: [.allCorners],
cornerRadii: CGSize(width: ivUser.frame.width/2, height: ivUser.frame.height/2)).cgPath

Thin border when using CAShapeLayer as mask for CAShapeLayer

In Swift, I have two semi-transparent circles, both of which are CAShapeLayer. Since they are semi-transparent, any overlap between them becomes visible like so:
Instead, I want them to visually "merge" together. The solution I have tried is to use circle 2 as a mask for circle 1, therefore cutting away the overlap.
This solution is generally working, but I get a thin line on the outside of circle 2:
My question: How can I get rid of the thin, outside line on the right circle? Why is it even there?
The code is as follows (Xcode playground can be found here):
private let yPosition: CGFloat = 200
private let circle1Position: CGFloat = 30
private let circle2Position: CGFloat = 150
private let circleDiameter: CGFloat = 200
private var circleRadius: CGFloat { return self.circleDiameter/2.0 }
override func loadView() {
let view = UIView()
view.backgroundColor = .black
self.view = view
let circle1Path = UIBezierPath(
roundedRect: CGRect(
x: circle1Position,
y: yPosition,
width: circleDiameter,
height: circleDiameter),
cornerRadius: self.circleDiameter)
let circle2Path = UIBezierPath(
roundedRect: CGRect(
x: circle2Position,
y: yPosition,
width: circleDiameter,
height: circleDiameter),
cornerRadius: self.circleDiameter)
let circle1Layer = CAShapeLayer()
circle1Layer.path = circle1Path.cgPath
circle1Layer.fillColor = UIColor.white.withAlphaComponent(0.6).cgColor
let circle2Layer = CAShapeLayer()
circle2Layer.path = circle2Path.cgPath
circle2Layer.fillColor = UIColor.white.withAlphaComponent(0.6).cgColor
self.view.layer.addSublayer(circle1Layer)
self.view.layer.addSublayer(circle2Layer)
//Create a mask from the surrounding rectangle of circle1, and
//then cut out where it overlaps circle2
let maskPath = UIBezierPath(rect: CGRect(x: circle1Position, y: yPosition, width: circleDiameter, height: circleDiameter))
maskPath.append(circle2Path)
maskPath.usesEvenOddFillRule = true
maskPath.lineWidth = 0
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.cgPath
maskLayer.fillColor = UIColor.black.cgColor
maskLayer.fillRule = kCAFillRuleEvenOdd
circle1Layer.mask = maskLayer
}
If both CAShapeLayers have the same alpha value, you could place them inside a new parent CALayer then set the alpha of the parent instead.

Shadow on UIView layer

I've make a path in order to mask my view:
let path = // create magic path (uiview bounds + 2 arcs)
let mask = CAShapeLayer()
mask.path = path.cgPath
view.layer.masksToBounds = false
view.layer.mask = mask
Up to here all ok.
Now I would like to add a shadow that follows path, is it possibile?
I try in several way, the last one is:
mask.shadowPath = path.cgPath
mask.shadowColor = UIColor.red.cgColor
mask.shadowOffset = CGSize(width: 10, height: 2.0)
mask.shadowOpacity = 0.5
But this produce a partial shadow and with color of the original view..
With debug view hierarchy:
Any advice?
Final result should be similar to this, but with shadow that "follows" arcs on path.
When you add a mask to a layer, it clips anything outside that mask - including the shadow. To achieve this you'll need to add a "shadow" view below your masked view, that has the same path as the mask.
Or add a shadow layer to the masked view's superview.
let view = UIView(frame: CGRect(x: 50, y: 70, width: 100, height: 60))
view.backgroundColor = .cyan
let mask = CAShapeLayer()
mask.path = UIBezierPath(roundedRect: view.bounds, cornerRadius: 10).cgPath
view.layer.mask = mask
let shadowLayer = CAShapeLayer()
shadowLayer.frame = view.frame
shadowLayer.path = UIBezierPath(roundedRect: view.bounds, cornerRadius: 10).cgPath
shadowLayer.shadowOpacity = 0.5
shadowLayer.shadowRadius = 5
shadowLayer.masksToBounds = false
shadowLayer.shadowOffset = .zero
let container = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
container.backgroundColor = .white
container.layer.addSublayer(shadowLayer)
container.addSubview(view)
If you're going to be using this elsewhere, you could create a ShadowMaskedView that contains the shadow layer, and the masked view - maybe with a path property.
You can try this extension:
extension UIView {
func dropShadow() {
self.layer.masksToBounds = false
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOpacity = 0.5
self.layer.shadowOffset = CGSize(width: -1, height: 1)
self.layer.shadowRadius = 1
self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath
self.layer.shouldRasterize = true
}
}

Corner radius for a button to align with a view below

There are tons of answers on SO for rounding a particular corner. The issue I'm running into is I'm trying to align a button corner to a rounded corner of the view below. Please see the image. The yellow view is rounded in 4 corners. I'm trying to get the close button to round off at top right to align with the yellow view's round corner. I've used the Swift 3 code below, but the button stays square. Can anyone please point out what is missing?
viewRaised is the yellow view.
Many thanks!
let path = UIBezierPath(roundedRect: btnCancel.layer.bounds, byRoundingCorners:[.topRight, .bottomLeft], cornerRadii: CGSize(width: 10, height: 10))
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
btnCancel.layer.mask = maskLayer
btnCancel.layer.masksToBounds = true
self.viewRaised.layer.cornerRadius = 10
self.viewRaised.layer.masksToBounds = false
self.viewRaised.layer.shadowOffset = CGSize(width: 5, height: 10);
self.viewRaised.layer.shadowRadius = 10;
self.viewRaised.layer.shadowOpacity = 0.5;
UPDATE:
Interesting thing is that the same code seems to be working but only for top left. See the second image.
self.viewRaised.layer.cornerRadius = 10
self.viewRaised.layer.masksToBounds = false
self.viewRaised.layer.shadowOffset = CGSize(width: 5, height: 10);
self.viewRaised.layer.shadowRadius = 10;
self.viewRaised.layer.shadowOpacity = 0.5;
let path = UIBezierPath(roundedRect: btnCancel.layer.bounds, byRoundingCorners:[.allCorners], cornerRadii: CGSize(width: 10, height: 10))
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
btnCancel.layer.mask = maskLayer
This is quite perplexing. I'm using XCode Version 8.0 beta 6 (8S201h)
Your btnCancel.layer.mask should be set to the yellow view.
You need to add btnCancel as a sublayer of of yellow view's parent.
Example (Swift 3.x) :
let yellowView = UIView(frame: CGRectMake(20.0, 20.0, 200.0, 200.0))
yellowView.backgroundColor = UIColor.yellowColor()
yellowView.layer.borderColor = UIColor.clearColor().CGColor
yellowView.layer.borderWidth = 1.0
yellowView.layer.cornerRadius = 10.0
self.view.addSubview(yellowView) // Add yellowView to self's main view
let btnCancel = UIView(frame: CGRectMake(20.0, 20.0, 45.0, 45.0))
btnCancel.backgroundColor = UIColor.redColor()
btnCancel.layer.mask = yellowView.layer // Set btnCancel.layer.mask to yellowView.layer
self.view.addSubview(btnCancel) // Add btnCancel to self's main view, NOT yellowView
NOTE:
You don't need to enable clipsToBounds because you're setting a mask layer.
You Also don't need to create a new CAShapeLayer for the mask. Use yellowView's layer as the mask.
Swift 4.x :
let yellowView = UIView(frame: CGRect(x: 20.0, y: 20.0, width: 200.0, height: 200.0))
yellowView.backgroundColor = .yellow
yellowView.layer.borderColor = UIColor.clear.cgColor
yellowView.layer.borderWidth = 1.0
yellowView.layer.cornerRadius = 10.0
self.view.addSubview(yellowView) // Add yellowView to self's main view
let btnCancel = UIView(frame: CGRect(x: 20.0, y: 20.0, width: 45.0, height: 45.0))
btnCancel.backgroundColor = .red
btnCancel.layer.mask = yellowView.layer // Set btnCancel.layer.mask to yellowView.layer
self.view.addSubview(btnCancel) // Add btnCancel to self's main view, NOT yellowView
All I needed to do was remove the whole of this. But it worked if I remove the bolded line only. But the rest of the lines were not needed once since Cancel Button was the subview of the yellow view.
let path = UIBezierPath(roundedRect: btnCancel.layer.bounds, byRoundingCorners:[.topRight, .bottomLeft], cornerRadii: CGSize(width: 10, height: 10))
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
btnCancel.layer.mask = maskLayer
btnCancel.layer.masksToBounds = true

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