So, I have a CALayer, which has a mask & I want to add border around this layer's mask. For example, I have set triangle mask to the layer and I want to have border around that layer.
Can anyone please help me to solve this problem?
Swift 4
class CustomView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
self.backgroundColor = UIColor.black
//setup path for mask and border
let halfHeight = self.bounds.height * 0.5
let maskPath = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [.topLeft, .bottomRight],
cornerRadii: CGSize(width: halfHeight,
height: halfHeight))
//setup MASK
self.layer.mask = nil;
let maskLayer = CAShapeLayer()
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.cgPath
self.layer.mask = maskLayer
//setup Border for Mask
let borderLayer = CAShapeLayer()
borderLayer.path = maskPath.cgPath
borderLayer.lineWidth = 25
borderLayer.strokeColor = UIColor.red.cgColor
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.frame = self.bounds
self.layer.addSublayer(borderLayer)
}
My approach in swift3.
// Usage:
self.btnGroup.roundCorner([.topRight, .bottomRight], radius: 4.0, borderColor: UIColor.red, borderWidth: 1.0)
// Apply round corner and border. An extension method of UIView.
public func roundCorner(_ corners: UIRectCorner, radius: CGFloat, borderColor: UIColor, borderWidth: CGFloat) {
let path = UIBezierPath.init(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
let borderPath = UIBezierPath.init(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let borderLayer = CAShapeLayer()
borderLayer.path = borderPath.cgPath
borderLayer.lineWidth = borderWidth
borderLayer.strokeColor = borderColor.cgColor
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.frame = self.bounds
self.layer.addSublayer(borderLayer)
}
Consider this example code:
- (void)drawRect:(CGRect)rect {
CAShapeLayer *maskLayer = [CAShapeLayer layer];
//Modify to your needs
CGFloat maskInsetWidth = 5.0f;
CGFloat maskInsetHeight = 5.0f;
CGFloat maskCornerRadius = 5.0f;
CGFloat borderWidth = 2.0f;
UIColor *borderColor = [UIColor blackColor];
CGRect insetRect = CGRectInset(self.bounds, maskInsetWidth, maskInsetHeight);
insetRect.size.width = MAX(insetRect.size.width, 0);
insetRect.size.height = MAX(insetRect.size.height, 0);
CGPathRef path = [UIBezierPath bezierPathWithRoundedRect:insetRect cornerRadius:maskCornerRadius].CGPath;
if (borderWidth > 0.0f && borderColor != nil) {
CAShapeLayer *borderLayer = [CAShapeLayer layer];
[borderLayer setPath:path];
[borderLayer setLineWidth:borderWidth * 2.0f];
[borderLayer setStrokeColor:borderColor.CGColor];
[borderLayer setFillColor:[UIColor clearColor].CGColor];
borderLayer.frame = self.bounds;
[self.layer addSublayer:borderLayer];
}
[maskLayer setPath:path];
[maskLayer setFillRule:kCAFillRuleEvenOdd];
maskLayer.frame = self.bounds;
[self.layer setMask:maskLayer];
}
Some suggestions:
Use an opaque shadow instead of a border (you will have a blurred effect).
Create another layer, set its background color with the color you want for your border, mask it with a mask slightly bigger than the one you already have to simulate the border width, and put it centered behind your layer (may not work with every shape).
Do a morphological operation on your mask image to calculate the border, for instance with the vImageDilate family of functions (more complicated, and may run into performance problems).
If you know the shape and it can be described mathematically, draw it and stroke it explicitly with Core Graphics functions.
Or, in the same case (shape known mathematically), use a CAShapeLayer to draw the border.
In a general case you cannot easily set a border around a mask. That's like asking to put a border around the transparent pixels of an image. Perhaps it may be done using image filters. In some more specific case, if you are using plain CAShapeLayer then here is a sample of code that does that:
[CATransaction begin];
[CATransaction setDisableActions:YES];
CALayer *hostLayer = [CALayer layer];
hostLayer.backgroundColor = [NSColor blackColor].CGColor;
hostLayer.speed = 0.0;
hostLayer.timeOffset = 0.0;
CALayer *maskedLayer = [CALayer layer];
maskedLayer.backgroundColor = [NSColor redColor].CGColor;
maskedLayer.position = CGPointMake(200, 200);
maskedLayer.bounds = CGRectMake(0, 0, 200, 200);
CAShapeLayer *mask = [CAShapeLayer layer];
mask.fillColor = [NSColor whiteColor].CGColor;
mask.position = CGPointMake(100, 100);
mask.bounds = CGRectMake(0, 0, 200, 200);
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 100, 100);
for (int i=0; i<20; i++) {
double x = arc4random_uniform(2000) / 10.0;
double y = arc4random_uniform(2000) / 10.0;
CGPathAddLineToPoint(path, NULL, x, y);
}
CGPathCloseSubpath(path);
mask.path = path;
CGPathRelease(path);
maskedLayer.mask = mask;
CAShapeLayer *maskCopy = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:mask]];
maskCopy.fillColor = NULL;
maskCopy.strokeColor = [NSColor yellowColor].CGColor;
maskCopy.lineWidth = 4;
maskCopy.position = maskedLayer.position;
// Alternately, don't set the position and add the copy as a sublayer
// maskedLayer.sublayers = #[maskCopy];
hostLayer.sublayers = #[maskedLayer,maskCopy];
_contentView.layer = hostLayer;
_contentView.wantsLayer = YES;
[CATransaction commit];
It basically creates an arbitrary path and sets it as the mask. It then takes a copy of this layer to stroke the path. You might need to tweak things to get the exact effect you are looking for.
If you subclass CALayer, you could instantiate it with the mask you want, and also override layoutSubLayers to include the border you want. This will work for all masks and should be the new accepted answer.
Could do this a couple ways. Below Ill do it by using the path of the given mask, and assigning that to class property to be used for constructing the new border in layoutSubLayers. There is potential that this method could be called multiple times, so I also set a boolean to track this. (Could also assign the border as a class property, and remove/re-add each time. For now I use bool check.
Swift 3:
class CustomLayer: CALayer {
private var path: CGPath?
private var borderSet: Bool = false
init(maskLayer: CAShapeLayer) {
super.init()
self.path = maskLayer.path
self.frame = maskLayer.frame
self.bounds = maskLayer.bounds
self.mask = maskLayer
}
override func layoutSublayers() {
if(!borderSet) {
self.borderSet = true
let newBorder = CAShapeLayer()
newBorder.lineWidth = 12
newBorder.path = self.path
newBorder.strokeColor = UIColor.black.cgColor
newBorder.fillColor = nil
self.addSublayer(newBorder)
}
}
required override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Related
I have a UIBezierPath inside my custom UIView draw(_ rect: CGRect) function. I would like to fill the path with a gradient color. Please can anybody guide me how can I do that.
I need to fill the clip with a gradient color and then stroke the path with black color.
There are some posts in SO which does not solve the problem. For example Swift: Gradient along a bezier path (using CALayers) this post guides how to draw on a layer in UIView but not in a UIBezierPath.
NB: I am working on Swift-3
To answer this question of yours,
I have a UIBezierPath inside my custom UIView draw(_ rect: CGRect)
function. I would like to fill the path with a gradient color.
Lets say you have an oval path,
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 100, height: 100))
To create a gradient,
let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]
We need a mask layer for gradient,
let shapeMask = CAShapeLayer()
shapeMask.path = path.cgPath
Now set this shapeLayer as mask of gradient layer and add it to view's layer as subLayer
gradient.mask = shapeMask
yourCustomView.layer.addSublayer(gradient)
Update
Create a base layer with stroke and add before creating gradient layer.
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.lineWidth = 2.0
shape.strokeColor = UIColor.black.cgColor
self.view.layer.addSublayer(shape)
let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]
let shapeMask = CAShapeLayer()
shapeMask.path = path.cgPath
gradient.mask = shapeMask
self.view.layer.addSublayer(gradient)
You can do this directly in Core Graphics without using CALayer classes. Use bezierPath.addClip() to set the bezier path as the clipping region. Any subsequent drawing commands will be masked to that region.
I use this wrapper function in one of my projects:
func drawLinearGradient(inside path:UIBezierPath, start:CGPoint, end:CGPoint, colors:[UIColor])
{
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.saveGState()
defer { ctx.restoreGState() } // clean up graphics state changes when the method returns
path.addClip() // use the path as the clipping region
let cgColors = colors.map({ $0.cgColor })
guard let gradient = CGGradient(colorsSpace: nil, colors: cgColors as CFArray, locations: nil)
else { return }
ctx.drawLinearGradient(gradient, start: start, end: end, options: [])
}
The other answers here don't seem to work (At least in newer iOS versions?)
Here's a working example of a bezier path being created and the stroke being a gradient stroke, adjust the CGPoints and CGColors as needed:
CGPoint start = CGPointMake(300, 400);
CGPoint end = CGPointMake(350, 400);
CGPoint control = CGPointMake(300, 365);
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:start];
[path addQuadCurveToPoint:end controlPoint:control];
CAShapeLayer *shapeLayer = [CAShapeLayer new];
shapeLayer.path = path.CGPath;
shapeLayer.strokeColor = [UIColor whiteColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 2.0;
//[self.view.layer addSublayer:shapeLayer];
CGRect gradientBounds = CGRectMake(path.bounds.origin.x-shapeLayer.lineWidth, path.bounds.origin.y-shapeLayer.lineWidth, path.bounds.size.width+shapeLayer.lineWidth*2.0, path.bounds.size.height+shapeLayer.lineWidth*2.0);
CAGradientLayer *gradientLayer = [CAGradientLayer new];
gradientLayer.frame = gradientBounds;
gradientLayer.bounds = gradientBounds;
gradientLayer.colors = #[(id)[UIColor redColor].CGColor, (id)[UIColor blueColor].CGColor];
gradientLayer.mask = shapeLayer;
[self.view.layer addSublayer:gradientLayer];
Swift 5 version of #AlbertRenshaw's (ObjC) answer, and wrapped in a UIView class.
What it displays looks spherical but on it's side.
A CGAffineTransform could be applied to rotate it 90º for a cool effect.
import UIKit
class SphereView : UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
let centerPoint = CGPoint(x: frame.width / 2, y: frame.height / 2)
let radius = frame.height / 2
let circlePath = UIBezierPath(arcCenter: centerPoint,
radius: radius,
startAngle: 0,
endAngle: .pi * 2,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath;
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 2.0;
let gradientBounds = CGRectMake(
circlePath.bounds.origin.x - shapeLayer.lineWidth,
circlePath.bounds.origin.y - shapeLayer.lineWidth,
circlePath.bounds.size.width + shapeLayer.lineWidth * 2.0,
circlePath.bounds.size.height + shapeLayer.lineWidth * 2.0)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = gradientBounds;
gradientLayer.bounds = gradientBounds;
gradientLayer.colors = [UIColor.red, UIColor.blue].map { $0.cgColor }
gradientLayer.mask = shapeLayer;
self.layer.addSublayer(gradientLayer)
}
}
Usage in a UIViewController:
override func viewDidLoad() {
view.backgroundColor = .white
let sphereView = SphereView()
sphereView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(sphereView)
NSLayoutConstraint.activate([
sphereView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
sphereView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
sphereView.widthAnchor.constraint(equalToConstant: 150),
sphereView.heightAnchor.constraint(equalToConstant: 150),
])
}
I have a problem clipping a view using CAShapeLayer-UIBezierPath , I want to clip the content but I end up getting a stroke (frame) with that UIBezierPath , This is my code
UIBezierPath *path2Path = [UIBezierPath bezierPath];
[path2Path moveToPoint:CGPointMake(206.745, 0)];
[path2Path addLineToPoint:CGPointMake(206.745, 97.613)];
[path2Path addLineToPoint:CGPointMake(0, 97.613)];
[path2Path addLineToPoint:CGPointMake(0, 0)];
[path2Path addLineToPoint:CGPointMake(87.28, 0)];
[path2Path addCurveToPoint:CGPointMake(103.808, 12.118) controlPoint1:CGPointMake(87.28, 0) controlPoint2:CGPointMake(86.555, 12.118)];
[path2Path addCurveToPoint:CGPointMake(119.466, 0) controlPoint1:CGPointMake(121.061, 12.118) controlPoint2:CGPointMake(119.466, 0)];
[path2Path addLineToPoint:CGPointMake(206.745, 0)];
[path2Path closePath];
[path2Path addClip];
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.frame=MYVIEW.bounds;
pathLayer.path = path2Path.CGPath;
pathLayer.strokeColor = [[UIColor blackColor] CGColor];
pathLayer.fillColor = [[UIColor clearColor] CGColor];
pathLayer.fillRule=kCAFillRuleEvenOdd;
[MYVIEW.layer setMask:pathLayer];
[MYVIEW.layer setMasksToBounds:YES];
MYVIEW.backgroundColor=[UIColor greenColor];
The result of this code is just a green stroke line ,the bounds is empty ,
like this
http://i.stack.imgur.com/aehdo.png
However , I want to make the bounds green , clipped by that stroke
as rob mayoff said You can do this easily by setting your view's layer mask to a CAShapeLayer.
UIBezierPath *myClippingPath = ...
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
myView.layer.mask = mask;
In Swift
let myClippingPath = UIBezierPath( ... )
let mask = CAShapeLayer()
mask.path = myClippingPath.CGPath
myView.layer.mask = mask
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.appendPath(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.appendPath(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 :)
You can even improve #Eugene's response (greate work, mate!) with corner radius
extension UIView {
func mask(withRect rect: CGRect, cornerRadius: CGFloat = 0, inverse: Bool = false) {
let path = cornerRadius > 0 ?
UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius) :
UIBezierPath(rect: rect)
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
}
My application is mostly round- and borderbased.
I use the layer property of UIView to give a corner radius and a border.
But I am facing a problem that the corners are not clear.
I am getting the following results:
UIButton
UIImageView
You can observe a thin border line around the white or grey border.
This is my code:
button.layer.borderWidth = 2.0;
button.layer.borderColor = [[UIColor whiteColor] CGColor];
button.layer.cornerRadius = 4;
button.clipsToBounds = YES;
I have searched to solve this but I'm not getting success.
I have tried button.layer.masksToBounds = YES, but with no effect.
Am I missing anything? Or are there any other methods which can give me better results compared to CALayer?
I tried many solution and end by using UIBezierPath.
I create category of UIView and add method to make round rect and border.
This is method of that category:
- (void)giveBorderWithCornerRadious:(CGFloat)radius borderColor:(UIColor *)borderColor andBorderWidth:(CGFloat)borderWidth
{
CGRect rect = self.bounds;
//Make round
// Create the path for to make circle
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:rect
byRoundingCorners:UIRectCornerAllCorners
cornerRadii:CGSizeMake(radius, radius)];
// Create the shape layer and set its path
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = rect;
maskLayer.path = maskPath.CGPath;
// Set the newly created shape layer as the mask for the view's layer
self.layer.mask = maskLayer;
//Give Border
//Create path for border
UIBezierPath *borderPath = [UIBezierPath bezierPathWithRoundedRect:rect
byRoundingCorners:UIRectCornerAllCorners
cornerRadii:CGSizeMake(radius, radius)];
// Create the shape layer and set its path
CAShapeLayer *borderLayer = [CAShapeLayer layer];
borderLayer.frame = rect;
borderLayer.path = borderPath.CGPath;
borderLayer.strokeColor = [UIColor whiteColor].CGColor;
borderLayer.fillColor = [UIColor clearColor].CGColor;
borderLayer.lineWidth = borderWidth;
//Add this layer to give border.
[[self layer] addSublayer:borderLayer];
}
I get idea of using UIBezierPath from this amazing article: Thinking like a Bézier path
I get most of code from this two link:
UIView category for rounding just the corners which you want, not all like CALayer cornerRadius.
How to get a border on UIBezierPath
Note: This is category method so self represent view on which this method is called. Like UIButton, UIImageView etc.
Here is a Swift 5 version of #CRDave's answer as an extension of UIView:
protocol CornerRadius {
func makeBorderWithCornerRadius(radius: CGFloat, borderColor: UIColor, borderWidth: CGFloat)
}
extension UIView: CornerRadius {
func makeBorderWithCornerRadius(radius: CGFloat, borderColor: UIColor, borderWidth: CGFloat) {
let rect = self.bounds
let maskPath = UIBezierPath(roundedRect: rect,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: radius, height: radius))
// Create the shape layer and set its path
let maskLayer = CAShapeLayer()
maskLayer.frame = rect
maskLayer.path = maskPath.cgPath
// Set the newly created shape layer as the mask for the view's layer
self.layer.mask = maskLayer
// Create path for border
let borderPath = UIBezierPath(roundedRect: rect,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: radius, height: radius))
// Create the shape layer and set its path
let borderLayer = CAShapeLayer()
borderLayer.frame = rect
borderLayer.path = borderPath.cgPath
borderLayer.strokeColor = borderColor.cgColor
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.lineWidth = borderWidth * UIScreen.main.scale
//Add this layer to give border.
self.layer.addSublayer(borderLayer)
}
}
This is Kamil Nomtek.com's answer updated to Swift 3+ and with some refinements (mostly semantics/naming and using a class protocol).
protocol RoundedBorderProtocol: class {
func makeBorder(with radius: CGFloat, borderWidth: CGFloat, borderColor: UIColor)
}
extension UIView: RoundedBorderProtocol {
func makeBorder(with radius: CGFloat, borderWidth: CGFloat, borderColor: UIColor) {
let maskPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: radius, height: radius))
// Create the shape layer and set its path
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = maskPath.cgPath
// Set the newly created shape layer as the mask for the view's layer
layer.mask = maskLayer
//Create path for border
let borderPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: radius, height: radius))
// Create the shape layer and set its path
let borderLayer = CAShapeLayer()
borderLayer.frame = bounds
borderLayer.path = borderPath.cgPath
borderLayer.strokeColor = borderColor.cgColor
borderLayer.fillColor = UIColor.clear.cgColor
//The border is in the center of the path, so only the inside is visible.
//Since only half of the line is visible, we need to multiply our width by 2.
borderLayer.lineWidth = borderWidth * 2
//Add this layer to display the border
layer.addSublayer(borderLayer)
}
}
The answer by CRDave works great but has one flaw: When called multiple times like on size change, it keeps adding layers. Instead previous layers should be updated.
See below for an updated ObjC version. For swift please adapt accordingly.
// UIView+Border.h
#import <UIKit/UIKit.h>
#interface UIView (Border)
- (void)setBorderWithCornerRadius:(CGFloat)radius
color:(UIColor *)borderColor
width:(CGFloat)borderWidth;
#end
// UIView+Border.m
#import "UIView+Border.h"
#implementation UIView (Border)
- (void)setBorderWithCornerRadius:(CGFloat)radius
color:(UIColor *)borderColor
width:(CGFloat)borderWidth {
CGRect rect = self.bounds;
//Make round
// Create the path for to make circle
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:rect
byRoundingCorners:UIRectCornerAllCorners
cornerRadii:CGSizeMake(radius, radius)];
// Create the shape layer and set its path
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = rect;
maskLayer.path = maskPath.CGPath;
// Set the newly created shape layer as the mask for the view's layer
self.layer.mask = maskLayer;
//Give Border
//Create path for border
UIBezierPath *borderPath = [UIBezierPath bezierPathWithRoundedRect:rect
byRoundingCorners:UIRectCornerAllCorners
cornerRadii:CGSizeMake(radius, radius)];
// Create the shape layer and set its path
NSString *layerName = #"ig_border_layer";
CAShapeLayer *borderLayer = (CAShapeLayer *)[[[self.layer sublayers] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"name == %#", layerName]] firstObject];
if (!borderLayer) {
borderLayer = [CAShapeLayer layer];
[borderLayer setName:layerName];
//Add this layer to give border.
[[self layer] addSublayer:borderLayer];
}
borderLayer.frame = rect;
borderLayer.path = borderPath.CGPath;
borderLayer.strokeColor = [UIColor whiteColor].CGColor;
borderLayer.fillColor = [UIColor clearColor].CGColor;
borderLayer.lineWidth = borderWidth;
}
#end
de.'s answer worked much better for me than CRDave's answer.
I had to translate it from Swift, so I thought I'd go ahead and post the translation:
extension UIView {
func giveBorderWithCornerRadius(cornerRadius r: CGFloat, borderColor c: UIColor, strokeWidth w: CGFloat) {
let rect = self.bounds
let maskPath = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: r, height: r))
let maskLayer = CAShapeLayer()
maskLayer.frame = rect
maskLayer.path = maskPath.cgPath
self.layer.mask = maskLayer
let borderPath = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: r, height: r))
let layerName = "border_layer"
var borderLayer: CAShapeLayer? = self.layer.sublayers?.filter({ (c) -> Bool in
if c.name == layerName {
return true
} else {
return false
}
}).first as? CAShapeLayer
if borderLayer == nil {
borderLayer = CAShapeLayer()
borderLayer!.name = layerName
self.layer.addSublayer(borderLayer!)
}
borderLayer!.frame = rect
borderLayer!.path = borderPath.cgPath
borderLayer!.strokeColor = c.cgColor
borderLayer!.fillColor = UIColor.clear.cgColor
borderLayer!.lineWidth = w
}
}
I'm calling the method from layoutSubviews()
delete
button.layer.borderWidth = 0.3;
button.layer.borderColor = [[UIColor blueMain] CGColor];
How would I mask CALayer with shadow, so that the shadow is only outside the path? I don't want shadow behind a transparent view.
shadowLayer.shadowOffset = CGSizeMake(x, y);
shadowLayer.shadowRadius = radius;
shadowLayer.shadowOpacity = opacity;
shadowLayer.shadowColor = color.CGColor;
shadowLayer.shadowPath = [UIBezierPath bezierPathWithRect:view.bounds].CGPath;
Thank you.
I will answer my own question. Adding a new shadow layer for view. This should work for any shadowPath, if set correctly.
float radius = 8;
float opacity = 0.5f;
float x = 4;
float y = 6;
UIColor *color = [UIColor blackColor];
// Shadow layer
CALayer *shadowLayer = [CALayer layer];
shadowLayer.shadowOffset = CGSizeMake(x, y);
shadowLayer.shadowRadius = radius;
shadowLayer.shadowOpacity = opacity;
shadowLayer.shadowColor = color.CGColor;
shadowLayer.shadowPath = [UIBezierPath bezierPathWithRect:view.frame].CGPath; // Or any other path
// Shadow mask frame
CGRect frame = CGRectInset(view.layer.frame, -2*radius, -2*radius);
frame = CGRectOffset(frame, x, y);
// Translate shadowLayer shadow path to mask layer's coordinate system
CGAffineTransform trans = CGAffineTransformMakeTranslation(-view.frame.origin.x-x+2*radius,
-view.frame.origin.y-y+2*radius);
// Mask path
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, nil, (CGRect){.origin={0,0}, .size=frame.size});
CGPathAddPath(path, &trans, shadowLayer.shadowPath);
CGPathCloseSubpath(path);
// Mask layer
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = frame;
maskLayer.fillRule = kCAFillRuleEvenOdd;
maskLayer.path = path;
shadowLayer.mask = maskLayer;
[view.layer.superlayer insertSublayer:shadowLayer below:view.layer];
ssteinberg's answer helped me in iOS 10.2 and Swift 3. I was able to use his answer with a UIVisualEffectView to create a beautiful blur but also with a shadow. Converted to Swift 3 here:
let shadowLayer = CALayer()
let mutablePath = CGMutablePath()
let maskLayer = CAShapeLayer()
let xOffset = CGFloat(4)
let yOffset = CGFloat(6)
let shadowOffset = CGSize(width: xOffset, height: yOffset)
let shadowOpacity = Float(0.5)
let shadowRadius = CGFloat(8)
let shadowPath = UIBezierPath(rect: frame).cgPath
let shadowColor = UIColor.black
let shadowFrame = frame.insetBy(dx: -2 * shadowRadius, dy: -2 * shadowRadius).offsetBy(dx: xOffset, dy: yOffset)
let shadowRect = CGRect(origin: .zero, size: shadowFrame.size)
let shadowTransform = CGAffineTransform(translationX: -frame.origin.x - xOffset + 2 * shadowRadius, y: -frame.origin.y - yOffset + 2 * shadowRadius)
shadowLayer.shadowOffset = shadowOffset
shadowLayer.shadowOpacity = shadowOpacity
shadowLayer.shadowRadius = shadowRadius
shadowLayer.shadowPath = shadowPath
shadowLayer.shadowColor = shadowColor.cgColor
mutablePath.addRect(shadowRect)
mutablePath.addPath(shadowLayer.shadowPath!, transform: shadowTransform)
mutablePath.closeSubpath()
maskLayer.frame = shadowFrame
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.path = mutablePath
shadowLayer.mask = maskLayer
layer.superlayer?.insertSublayer(shadowLayer, above: layer)
Just an advance: u can use CAShapeLayer to set shadow
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 2
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.purple.cgColor
shapeLayer.shadowColor = UIColor.black.cgColor
shapeLayer.shadowOffset = CGSize(width: 0, height: 3)
shapeLayer.shadowOpacity = 1
view.layer.addSublayer(shapeLayer)
I have the following code in a category that does rounding of corners. I would also like to draw a border. But the border is not shown on the rounded part of the corner.
Here is the code
- (void) roundTopCorners:(CGFloat) radius
{
self.layer.masksToBounds = YES;
CGRect bounds = self.bounds;
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:bounds byRoundingCorners:(UIRectCornerTopLeft | UIRectCornerTopRight) cornerRadii:CGSizeMake(radius, radius)];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = bounds;
maskLayer.path = maskPath.CGPath;
maskLayer.strokeColor = [UIColor redColor].CGColor;
self.layer.mask = maskLayer;
}
The mask layer doesn't get drawn, just used to compute the mask. Try:
-(void)roundCorners:(UIRectCorner)corners radius:(CGFloat)radius
{
CGRect bounds = self.bounds;
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:bounds
byRoundingCorners:corners
cornerRadii:CGSizeMake(radius, radius)];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = bounds;
maskLayer.path = maskPath.CGPath;
self.layer.mask = maskLayer;
CAShapeLayer* frameLayer = [CAShapeLayer layer];
frameLayer.frame = bounds;
frameLayer.path = maskPath.CGPath;
frameLayer.strokeColor = [UIColor redColor].CGColor;
frameLayer.fillColor = nil;
[self.layer addSublayer:frameLayer];
}
-(void)roundTopCornersRadius:(CGFloat)radius
{
[self roundCorners:(UIRectCornerTopLeft|UIRectCornerTopRight) radius:radius];
}
-(void)roundBottomCornersRadius:(CGFloat)radius
{
[self roundCorners:(UIRectCornerBottomLeft|UIRectCornerBottomRight) radius:radius];
}
The frame you're currently seeing drawn is the UITextField's normal frame, so set the frame style to none. You'll also have to adjust the insets to make up for the fact that with the frame style set to none there's normally no inset.
Swift version of David Berry's answer:
func roundCorners(corners:UIRectCorner, radius:CGFloat) {
let bounds = self.bounds
let maskPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSizeMake(radius, radius))
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = maskPath.CGPath
self.layer.mask = maskLayer
let frameLayer = CAShapeLayer()
frameLayer.frame = bounds
frameLayer.path = maskPath.CGPath
frameLayer.strokeColor = UIColor.redColor().CGColor
frameLayer.fillColor = nil
self.layer.addSublayer(frameLayer)
}
func roundTopCornersRadius(radius:CGFloat) {
self.roundCorners([UIRectCorner.TopLeft, UIRectCorner.TopRight], radius:radius)
}
func roundBottomCornersRadius(radius:CGFloat) {
self.roundCorners([UIRectCorner.BottomLeft, UIRectCorner.BottomRight], radius:radius)
}
this is probably a very late answer but I would just like to share the solution I came up with based on answers of different people from different similar questions. I got big help from Vojtech's answer above.
extension UIView {
func EZRoundCorners(corners:UIRectCorner, radius: CGFloat) -> CAShapeLayer {
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.CGPath
self.layer.mask = mask
return mask
}
func EZRoundCornersWithBorder(corners:UIRectCorner, radius:CGFloat, color:UIColor, width:CGFloat) -> CAShapeLayer {
let mask = self.EZRoundCorners(corners, radius: radius)
// Add border
let borderLayer = EZCALayer()
borderLayer.path = mask.path // Reuse the Bezier path
borderLayer.fillColor = UIColor.clearColor().CGColor
borderLayer.strokeColor = color.CGColor
borderLayer.lineWidth = width
borderLayer.frame = self.bounds
self.layer.addSublayer(borderLayer)
return borderLayer
}
func removeEZLayers () {
for layer in self.layer.sublayers! {
if layer is EZCALayer {
layer.removeFromSuperlayer()
}
}
}
}
class EZCALayer : CAShapeLayer {
}
I inherited from CAShapeLayer so I can remove the border sublayers if I don't want to use them anymore.
label.layer.cornerRadius = 3.0
label.layer.maskedCorners = [.layerMinXMinYCorner,.layerMinXMaxYCorner]//round top left and bottom left corners
Source:https://www.hackingwithswift.com/example-code/calayer/how-to-round-only-specific-corners-using-maskedcorners
Image of working solution