Swift Draw Shadow to a Uibezier Path

Swift draw shadow to a uibezier path

I take this example straight from my PaintCode-app. Hope this helps.

//// General Declarations
let context = UIGraphicsGetCurrentContext()

//// Shadow Declarations
let shadow = UIColor.blackColor()
let shadowOffset = CGSizeMake(3.1, 3.1)
let shadowBlurRadius: CGFloat = 5

//// Bezier 2 Drawing
var bezier2Path = UIBezierPath()
bezier2Path.moveToPoint(CGPointMake(30.5, 90.5))
bezier2Path.addLineToPoint(CGPointMake(115.5, 90.5))
CGContextSaveGState(context)
CGContextSetShadowWithColor(context, shadowOffset, shadowBlurRadius, (shadow as UIColor).CGColor)
UIColor.blackColor().setStroke()
bezier2Path.lineWidth = 1
bezier2Path.stroke()
CGContextRestoreGState(context)

Adding shadow to UIView with UIBezierPath

Here is how you can get Shadow

import UIKit

@IBDesignable
class ArrowView: UIView {

private lazy var arrowLayer : CALayer = {
let layer = CALayer()
layer.backgroundColor = UIColor.red.cgColor
return layer
}()

override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}

required init?(coder: NSCoder) {
super.init(coder: coder)

setupView()

}
func setupView() {
self.backgroundColor = .clear
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 1
layer.shadowOffset = .zero
layer.shadowRadius = 10

layer.addSublayer(arrowLayer)
}

private func updatePath() {
let path = UIBezierPath()
let edge:CGFloat = 20.0
path.move(to: CGPoint(x: 0.0, y: 0.0))
path.addLine(to: CGPoint(x: self.bounds.size.width - edge, y: 0.0))
path.addLine(to: CGPoint(x: self.bounds.size.width , y: self.bounds.size.height/2))
path.addLine(to: CGPoint(x: self.bounds.size.width - edge , y: self.bounds.size.height))
path.addLine(to: CGPoint(x: 0, y: self.bounds.size.height))
path.close()

arrowLayer.frame = self.bounds

let mask = CAShapeLayer()
mask.path = path.cgPath

arrowLayer.mask = mask
}

override func layoutSubviews() {
updatePath()
}

}

Sample Image

Add Shadow to CAShapeLayer with UIBezierPath

Output:

Sample Image

UIView Extension:

extension UIView {
func renderCircle() {
let semiCircleLayer = CAShapeLayer()

let center = CGPoint (x: self.frame.size.width / 2, y: self.frame.size.height / 2)
let circleRadius = self.frame.size.width / 2
let circlePath = UIBezierPath(arcCenter: center, radius: circleRadius, startAngle: CGFloat(Double.pi * 2), endAngle: CGFloat(Double.pi), clockwise: true)

semiCircleLayer.path = circlePath.cgPath
semiCircleLayer.strokeColor = UIColor.red.cgColor
semiCircleLayer.fillColor = UIColor.clear.cgColor
semiCircleLayer.lineWidth = 8
semiCircleLayer.shadowColor = UIColor.red.cgColor
semiCircleLayer.shadowRadius = 25.0
semiCircleLayer.shadowOpacity = 1.0
semiCircleLayer.shadowPath = circlePath.cgPath.copy(strokingWithWidth: 25, lineCap: .round, lineJoin: .miter, miterLimit: 0)

semiCircleLayer.shadowOffset = CGSize(width: 1.0, height: 1.0)
self.layer.addSublayer(semiCircleLayer)
}
}

Usage:

semiCircleView.renderCircle()

Adding Corner radius for selected edges with shadow using UIBezierPath

If you are targeting iOS 11+ you can use the layer's .maskedCorners property:

class BubbleView: UIView {

// don't override draw()
// override func draw(_ rect: CGRect) {
// super.draw(rect)
// }

override func layoutSubviews() {
super.layoutSubviews()
self.updateContainerLayer()
}

func updateContainerLayer() {
let brazierPath: UIBezierPath = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [.bottomRight, .bottomLeft, .topLeft],
cornerRadii: CGSize(width: 15.0, height: 0.0))

//1
// let shapeLayer = CAShapeLayer()
// shapeLayer.path = brazierPath.cgPath
// self.layer.mask = shapeLayer

// iOS 11+ use .maskedCorners
self.layer.cornerRadius = 15.0
self.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]

//2
self.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.25).cgColor
self.layer.shadowOpacity = 1.0
self.layer.shadowOffset = CGSize(width: 0.0, height: 0.5)
self.layer.shadowRadius = 1.5
self.layer.shadowPath = brazierPath.cgPath
//3
self.layer.masksToBounds = true
self.clipsToBounds = false
//4
self.layer.shouldRasterize = true
self.layer.rasterizationScale = UIScreen.main.scale

}
}

Result:

Sample Image

Result with an exaggerated .shadowOffset = CGSize(width: -10.0, height: 10.5) to make it easy to see the shadow:

Sample Image

If you need to allow earlier iOS versions, I believe you'll need to use a container view approach.


EDIT:

Another approach, using a "container" view for the shadow. This will work with iOS earlier than 11... it uses the same UIBezierPath for the "content view" mask and the shadow path:

class BubbleView: UIView {

let contentView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()

override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}

func commonInit() -> Void {

addSubview(contentView)

NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
])

self.clipsToBounds = false

backgroundColor = .clear
contentView.backgroundColor = .red

// set non-changing properties here
contentView.layer.masksToBounds = true

self.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.25).cgColor
self.layer.shadowOpacity = 1.0

self.layer.shadowOffset = CGSize(width: 0.0, height: 0.5)

// exaggerated shadow offset so we can see it easily
//self.layer.shadowOffset = CGSize(width: -10.0, height: 10.5)

self.layer.shadowRadius = 1.5

self.layer.shouldRasterize = true
self.layer.rasterizationScale = UIScreen.main.scale

}

override func layoutSubviews() {
super.layoutSubviews()

let bezierPath = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [.bottomRight, .bottomLeft, .topLeft],
cornerRadii: CGSize(width: 15.0, height: 0.0))

let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
contentView.layer.mask = shapeLayer

self.layer.shadowPath = bezierPath.cgPath

}

}

As with the 11+ example, Result:

Sample Image

and Result with an exaggerated .shadowOffset = CGSize(width: -10.0, height: 10.5) to make it easy to see the shadow:

Sample Image

how to convert UIBezierpath with shadow to UIImage without losing the shadow saturation

For reasons I don't understand, shadows are not rendered correctly when you use a layer's render(in:) method.

However, if your layers are added to a view, you can use that view's drawHierarchy(in: afterScreenUpdates:), which will faithfully render the shadow.

This playground code:

import UIKit
import PlaygroundSupport

let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = .black
PlaygroundPage.current.liveView = view

let layer = CAShapeLayer()
let path = UIBezierPath()
path.move(to: CGPoint(x: 20, y: 50))
path.addLine(to: CGPoint(x: 180, y: 50))
layer.path = path.cgPath
layer.lineCap = .round
layer.fillColor = nil
layer.lineWidth = 4
layer.strokeColor = UIColor.white.cgColor
layer.shadowPath = path.cgPath.copy(strokingWithWidth: 16, lineCap: .round, lineJoin: .round, miterLimit: 0)
layer.shadowOffset = .zero
layer.shadowColor = UIColor.yellow.cgColor
layer.shadowRadius = 5
layer.shadowOpacity = 0.7
layer.frame = CGRect(x:0, y: 0, width: 200, height: 100)
view.layer.addSublayer(layer)
layer.shouldRasterize = true
let renderer = UIGraphicsImageRenderer(bounds: layer.bounds)
let image = renderer.image { context in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
let imageView = UIImageView(image: image)
view.addSubview(imageView)
imageView.frame.origin.y = 100

Gives you this output:

Sample Image

I'd love to know why shadows don't render properly, though.

Shadow set on uibezierpath in draw(rect:) removed when view is redrawn

Aight was able to figure this one out. The bezier path shadow was getting hidden when the view was redrawn because the layer mask was getting set to the shapeLayer. My solution was to draw the curved shadow using the layer's shadowPath property and inserting the shapeLayer as a subLayer instead of masking the layer. Make sure to add the shapeLayer before you add the gradient, so the gradient appears on top of the shapeLayer. The didSetGradient block is pretty tacky as @dfd mentioned, but it's working for now!

override func draw(_ rect: CGRect) {
super.draw(rect)

let p1 = CGPoint(x: 0, y: self.frame.height * 0.8)
let p2 = CGPoint(x: self.frame.width / 2, y: self.frame.height * 1.06)
let p3 = CGPoint(x: self.frame.width, y: self.frame.height * 0.8)
let p4 = CGPoint(x: self.frame.width, y: 0)
let p5 = CGPoint(x: 0, y: 0)

let path = UIBezierPath()

path.move(to: p1)
path.addQuadCurve(to: p3, controlPoint: p2)
path.addLine(to: p4)
path.addLine(to: p5)
path.addLine(to: p1)
path.close()

self.shapeLayer.path = path.cgPath

if !self.didSetGradient {
self.layer.insertSublayer(shapeLayer, at: 0)

self.gradient = CustomColors.blueGreenBackgroundGradient(frame: path.bounds)
self.gradient.mask = shapeLayer
self.gradient.masksToBounds = true
self.layer.insertSublayer(self.gradient, at: 0)

self.didSetGradient = true
}

self.layer.shadowPath = startPath.cgPath
self.layer.shadowColor = CustomColors.blueGreen.cgColor
self.layer.shadowOpacity = 0.5
self.layer.shadowRadius = 8
self.layer.shadowOffset = CGSize(width: 0, height: 7)
self.layer.masksToBounds = false
self.clipsToBounds = false
}

UIBezierPath Shadow with transparent internal rect Objective c

You can do this by

  • adding a CAShapeLayer as a sublayer
  • give it a rounded-rect path
  • give the path a White fill color
  • then use a "mask with a rectangle cut out of the center"

Here's a quick example view subclass and a controller demonstrating it:

Custom UIView subclass

class ShadowPathView: UIView {

let radius: CGFloat = 10

let shadowLayer = CAShapeLayer()
let maskLayer = CAShapeLayer()

override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {

// these properties don't change
backgroundColor = .clear

layer.addSublayer(shadowLayer)

shadowLayer.fillColor = UIColor.white.cgColor
shadowLayer.shadowColor = UIColor.red.cgColor
shadowLayer.shadowOpacity = 1.0
shadowLayer.shadowOffset = .zero

// set the layer mask
shadowLayer.mask = maskLayer
}

override func layoutSubviews() {
super.layoutSubviews()

shadowLayer.frame = bounds
shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: radius).cgPath

// create a rect bezier path, large enough to exceed the shadow bounds
let bez = UIBezierPath(rect: bounds.insetBy(dx: -radius * 2.0, dy: -radius * 2.0))

// create a path for the "hole" in the layer
let holePath = UIBezierPath(rect: bounds.insetBy(dx: radius, dy: radius))

// this "cuts a hole" in the path
bez.append(holePath)
bez.usesEvenOddFillRule = true
maskLayer.fillRule = .evenOdd

// set the path of the mask layer
maskLayer.path = bez.cgPath

let w: CGFloat = 5
// make the shadow rect larger than bounds
let shadowRect = bounds.insetBy(dx: -w, dy: -w)
// set the shadow path
// make the corner radius larger to make the curves look correct
shadowLayer.shadowPath = UIBezierPath(roundedRect: shadowRect, cornerRadius: radius + w).cgPath

}

}

Example view controller

class ShadowPathVC: UIViewController {

// two of our custom ShadowPathView
let v1 = ShadowPathView()
let v2 = ShadowPathView()

// a label to put UNDER the second view
let underLabel = UILabel()

// a label to add as a SUVBVIEW of the second view
let subLabel = UILabel()

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = UIColor(red: 0.8, green: 0.92, blue: 0.97, alpha: 1.0)

[underLabel, subLabel].forEach { v in
v.textAlignment = .center
v.backgroundColor = .green
}
[v1, v2, underLabel, subLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
[v1, underLabel, v2].forEach { v in
view.addSubview(v)
}
v2.addSubview(subLabel)
underLabel.text = "This label is Under the shadow view"
subLabel.text = "This label is a subview of the shadow view"
subLabel.numberOfLines = 0

let g = view.safeAreaLayoutGuide

NSLayoutConstraint.activate([

v1.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
v1.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
v1.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
v1.heightAnchor.constraint(equalToConstant: 120.0),

v2.topAnchor.constraint(equalTo: v1.bottomAnchor, constant: 80.0),
v2.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
v2.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
v2.heightAnchor.constraint(equalToConstant: 160.0),

underLabel.leadingAnchor.constraint(equalTo: v2.leadingAnchor, constant: -20.0),
underLabel.topAnchor.constraint(equalTo: v2.topAnchor, constant: -20.0),
underLabel.heightAnchor.constraint(equalToConstant: 80.0),

subLabel.bottomAnchor.constraint(equalTo: v2.bottomAnchor, constant: -12.0),
subLabel.trailingAnchor.constraint(equalTo: v2.trailingAnchor, constant: -40.0),
subLabel.widthAnchor.constraint(equalToConstant: 120.0),

])
}
}

How it looks:

Sample Image


Edit - I should have caught the need for Objective-C implementation (fewer and fewer requests for that theses days).

So, here's the same as above, but in Obj-C (default headers):

Custom UIView subclass

#import "ShadowPathView.h"

@interface ShadowPathView ()
{
CAShapeLayer *shadowLayer;
CAShapeLayer *maskLayer;
CGFloat radius;
}
@end

@implementation ShadowPathView

- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}

- (void) commonInit {
radius = 10;
shadowLayer = [CAShapeLayer new];
maskLayer = [CAShapeLayer new];

self.backgroundColor = [UIColor clearColor];

[self.layer addSublayer:shadowLayer];

shadowLayer.fillColor = [UIColor whiteColor].CGColor;
shadowLayer.shadowColor = [UIColor redColor].CGColor;
shadowLayer.shadowOpacity = 1.0;
shadowLayer.shadowOffset = CGSizeZero;

// set the layer mask
shadowLayer.mask = maskLayer;
}

- (void)layoutSubviews {
[super layoutSubviews];

shadowLayer.frame = self.bounds;
shadowLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:radius].CGPath;

// create a rect bezier path, large enough to exceed the shadow bounds
UIBezierPath *bez = [UIBezierPath bezierPathWithRect:CGRectInset(self.bounds, -radius, -radius)];

// create a path for the "hole" in the layer
UIBezierPath *holePath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, 0, 0) cornerRadius:radius];

// this "cuts a hole" in the path
[bez appendPath:holePath];
bez.usesEvenOddFillRule = YES;
maskLayer.fillRule = kCAFillRuleEvenOdd;

// set the path of the mask layer
maskLayer.path = bez.CGPath;

CGFloat shadowWidth = 5;
// make the shadow rect larger than bounds
CGRect shadowRect = CGRectInset(self.bounds, -shadowWidth, -shadowWidth);
// set the shadow path
// make the corner radius larger to make the curves look correct
shadowLayer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:shadowRect cornerRadius:radius + shadowWidth].CGPath;
}

@end

Example view controller

#import "ViewController.h"
#import "ShadowPathView.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.view.backgroundColor = [UIColor colorWithRed:0.8 green:0.92 blue:0.97 alpha:1.0];

// two of our custom ShadowPathView
ShadowPathView *v1 = [ShadowPathView new];
ShadowPathView *v2 = [ShadowPathView new];

// a label to put UNDER the second view
UILabel *underLabel = [UILabel new];

// a label to add as a SUVBVIEW of the second view
UILabel *subLabel = [UILabel new];

for (UILabel *v in @[underLabel, subLabel]) {
v.textAlignment = NSTextAlignmentCenter;
v.backgroundColor = [UIColor greenColor];
}
for (UIView *v in @[v1, v2, underLabel, subLabel]) {
v.translatesAutoresizingMaskIntoConstraints = NO;
}
for (UIView *v in @[v1, underLabel, v2]) {
[self.view addSubview:v];
}
[v2 addSubview:subLabel];
underLabel.text = @"This label is Under the shadow view";
subLabel.text = @"This label is a subview of the shadow view";
subLabel.numberOfLines = 0;

UILayoutGuide *g = self.view.safeAreaLayoutGuide;

[NSLayoutConstraint activateConstraints:@[

[v1.topAnchor constraintEqualToAnchor:g.topAnchor constant:40.0],
[v1.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:40.0],
[v1.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-40.0],
[v1.heightAnchor constraintEqualToConstant:120.0],

[v2.topAnchor constraintEqualToAnchor:v1.bottomAnchor constant:80.0],
[v2.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:40.0],
[v2.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-40.0],
[v2.heightAnchor constraintEqualToConstant:160.0],

[underLabel.leadingAnchor constraintEqualToAnchor:v2.leadingAnchor constant:-20.0],
[underLabel.topAnchor constraintEqualToAnchor:v2.topAnchor constant:-20.0],
[underLabel.heightAnchor constraintEqualToConstant:80.0],

[subLabel.bottomAnchor constraintEqualToAnchor:v2.bottomAnchor constant:-12.0],
[subLabel.trailingAnchor constraintEqualToAnchor:v2.trailingAnchor constant:-40.0],
[subLabel.widthAnchor constraintEqualToConstant:120.0],

]];

}

@end

add Shadow on UIView using swift 3

CODE SNIPPET:

extension UIView {

// OUTPUT 1
func dropShadow(scale: Bool = true) {
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.5
layer.shadowOffset = CGSize(width: -1, height: 1)
layer.shadowRadius = 1

layer.shadowPath = UIBezierPath(rect: bounds).cgPath
layer.shouldRasterize = true
layer.rasterizationScale = scale ? UIScreen.main.scale : 1
}

// OUTPUT 2
func dropShadow(color: UIColor, opacity: Float = 0.5, offSet: CGSize, radius: CGFloat = 1, scale: Bool = true) {
layer.masksToBounds = false
layer.shadowColor = color.cgColor
layer.shadowOpacity = opacity
layer.shadowOffset = offSet
layer.shadowRadius = radius

layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath
layer.shouldRasterize = true
layer.rasterizationScale = scale ? UIScreen.main.scale : 1
}
}

NOTE: If you don't pass any parameter to that function, then the scale argument will be true by default. You can define a default value for any parameter in a function by assigning a value to the parameter after that parameter’s type. If a default value is defined, you can omit that parameter when calling the function.

OUTPUT 1:

shadowView.dropShadow()

Sample Image

OUTPUT 2:

shadowView.dropShadow(color: .red, opacity: 1, offSet: CGSize(width: -1, height: 1), radius: 3, scale: true)

Sample Image

layer.shouldRasterize = true will make the shadow static and cause a shadow for the initial state of the UIView. So I would recommend not to use layer.shouldRasterize = true in dynamic layouts like view inside a UITableViewCell.

UIView with rounded corners and drop shadow?

The following code snippet adds a border, border radius, and drop shadow to v, a UIView:

// border radius
[v.layer setCornerRadius:30.0f];

// border
[v.layer setBorderColor:[UIColor lightGrayColor].CGColor];
[v.layer setBorderWidth:1.5f];

// drop shadow
[v.layer setShadowColor:[UIColor blackColor].CGColor];
[v.layer setShadowOpacity:0.8];
[v.layer setShadowRadius:3.0];
[v.layer setShadowOffset:CGSizeMake(2.0, 2.0)];

Swift 5 Version :

// border radius
v.layer.cornerRadius = 30.0

// border
v.layer.borderColor = UIColor.lightGray.cgColor
v.layer.borderWidth = 1.5

// drop shadow
v.layer.shadowColor = UIColor.black.cgColor
v.layer.shadowOpacity = 0.8
v.layer.shadowRadius = 3.0
v.layer.shadowOffset = CGSize(width: 2.0, height: 2.0)

You can adjust the settings to suit your needs.

Also, add the QuartzCore framework to your project and:

#import <QuartzCore/QuartzCore.h>

See my other answer regarding masksToBounds.


Note

This may not work in all cases. If you find that this method interferes with other drawing operations that you are performing, please see this answer.



Related Topics



Leave a reply



Submit