I have created a TicketView using Bezier Paths, which looks like this:
Now I am trying to add a shadow to it, but I encountered a problem. I also have an image on the left side of this view, and the rounded corners of this ticket view is supposed to work as a mask to it. However, when I apply the mask, the shadow does not work as expected. So far, the only workaround I have found is to add a container view behind the TicketView and apply the shadow to it. Unfortunately, this approach does not make the shadow follow the custom path I created for the TicketView.
Is there any way to apply a shadow that will correctly follow the custom path I created, while still keeping the mask applied?
Here is the code for reference:
import UIKit
/// A card view with rounded corners on the left side and a zig-zag shape on the right side
class TicketView: UIView {
/// The radius used for the left side of this view
let radius: CGFloat
/// The zig-zag's width
let zigZagsWidth: CGFloat
/// The position within the card's view where the zig-zag starts (1.0 = the end of the view)
let zigZagStartPosition: CGFloat
public init(
radius: CGFloat = 10,
zigZagsWidth: CGFloat = 5,
zigZagStartPosition: CGFloat = 1.0,
frame: CGRect = .zero
) {
self.radius = radius
self.zigZagsWidth = zigZagsWidth
self.zigZagStartPosition = zigZagStartPosition
super.init(frame: frame)
self.backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.5
layer.shadowRadius = 5.0
layer.shadowOffset = CGSize(width: 0, height: 0)
layer.shadowPath = (layer.mask as? CAShapeLayer)?.path
}
public override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
let path = UIBezierPath()
// Top-Left
let topLeftCorner = CGPoint(x: bounds.minX, y: bounds.minY)
let bottomLeftCorner = CGPoint(x: bounds.minX, y: bounds.maxY)
path.move(to: CGPoint(x: topLeftCorner.x, y: topLeftCorner.y))
path.addArc(withCenter: CGPoint(x: topLeftCorner.x + radius, y: topLeftCorner.y + radius),
radius: radius,
startAngle: .pi,
endAngle: .pi * 1.5,
clockwise: true)
// Zig-Zag
let zigzagHeight: CGFloat = rect.size.height / (rect.size.height / 7.3)
let zigzagWidth: CGFloat = zigZagsWidth
let numberOfSegments = Int(rect.size.height / zigzagHeight) + 1
for index in 0..<numberOfSegments {
let inY = CGFloat(index) * zigzagHeight
let inX = (index % 2 == 0) ? rect.size.width * zigZagStartPosition :
rect.size.width * zigZagStartPosition - zigzagWidth
path.addLine(to: CGPoint(x: inX, y: inY))
let zigZagCurveRadius: CGFloat = 2
let zigZagCurveCenter = CGPoint(
x: inX + zigZagCurveRadius,
y: inY + zigZagCurveRadius
)
path.addArc(withCenter: zigZagCurveCenter,
radius: zigZagCurveRadius,
startAngle: .pi,
endAngle: .pi,
clockwise: true)
}
// Bottom-Left
path.addLine(to: CGPoint(x: bottomLeftCorner.x + radius, y: bottomLeftCorner.y))
path.addArc(withCenter: CGPoint(x: bottomLeftCorner.x + radius, y: bottomLeftCorner.y - radius),
radius: radius,
startAngle: .pi * 1.5,
endAngle: .pi,
clockwise: true)
path.close()
context.setFillColor(UIColor.white.cgColor)
path.fill()
// Mask
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
The problem is that the layer you are masking is the same layer that you apply the shadow to. This causes the shadow to be masked, i.e. not visible.
Adding a container view works and applying the shadow to the container view works, because now the mask is applied to the view in the container, and the shadow is on the container, so the shadow is not masked away.
You can also add a subview inside TicketView
- the shadow is applied to TicketView
, but the mask is applied to the subview.
Or, just add a sublayer. The shadow is applied to TicketView.layer
, but the mask is applied to the sublayer.
I'm not sure how you observed "this approach does not make the shadow follow the custom path I created for the TicketView
". If you have set shadowPath
to the correct path, then it would work. That said, the actual path of the shadows on zig zags like this can be hard to see. To see that this does work, exaggerate the zigzag width, and reduce the shadow radius. You should see that the shadow path follows the zigzag.
I've used the "apply shadow to TicketView
, and apply mask to a subview of TicketView
" approach to create that. Full code (things I've changed are addressed in the comments):
class TicketView: UIView {
let radius: CGFloat
let zigZagsWidth: CGFloat
let zigZagStartPosition: CGFloat
// the subview that we are applying a mask to
let maskedSubview: UIView
public init(
radius: CGFloat = 10,
zigZagsWidth: CGFloat = 50, // exaggerated this
zigZagStartPosition: CGFloat = 1.0,
frame: CGRect = .zero
) {
self.radius = radius
self.zigZagsWidth = zigZagsWidth
self.zigZagStartPosition = zigZagStartPosition
// create the subview in init
maskedSubview = UIView()
maskedSubview.backgroundColor = .white
super.init(frame: frame)
addSubview(maskedSubview)
maskedSubview.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
maskedSubview.topAnchor.constraint(equalTo: topAnchor),
maskedSubview.bottomAnchor.constraint(equalTo: bottomAnchor),
maskedSubview.leadingAnchor.constraint(equalTo: leadingAnchor),
maskedSubview.trailingAnchor.constraint(equalTo: trailingAnchor),
])
// set shadow properties except shadowPath here
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.5
layer.shadowRadius = 1 // made this smaller to see more clearly
layer.shadowOffset = CGSize(width: 0, height: 0)
self.backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
// layoutSubviews is called, so the view bounds might have changed
// recompute the mask path
let mask = CAShapeLayer()
mask.path = makeTicketPath().cgPath
// apply mask to maskedSubview.layer
maskedSubview.layer.mask = mask
// apply shadow to self.layer
layer.shadowPath = mask.path
}
// I've extracted the path-drawing function:
private func makeTicketPath() -> UIBezierPath {
let path = UIBezierPath()
let rect = bounds
let topLeftCorner = CGPoint(x: bounds.minX, y: bounds.minY)
let bottomLeftCorner = CGPoint(x: bounds.minX, y: bounds.maxY)
path.move(to: CGPoint(x: topLeftCorner.x, y: topLeftCorner.y))
path.addArc(withCenter: CGPoint(x: topLeftCorner.x + radius, y: topLeftCorner.y + radius),
radius: radius,
startAngle: .pi,
endAngle: .pi * 1.5,
clockwise: true)
let zigzagHeight: CGFloat = rect.size.height / (rect.size.height / 7.3)
let zigzagWidth: CGFloat = zigZagsWidth
let numberOfSegments = Int(rect.size.height / zigzagHeight) + 1
for index in 0..<numberOfSegments {
let inY = CGFloat(index) * zigzagHeight
let inX = (index % 2 == 0) ? rect.size.width * zigZagStartPosition :
rect.size.width * zigZagStartPosition - zigzagWidth
path.addLine(to: CGPoint(x: inX, y: inY))
let zigZagCurveRadius: CGFloat = 2
let zigZagCurveCenter = CGPoint(
x: inX + zigZagCurveRadius,
y: inY + zigZagCurveRadius
)
path.addArc(withCenter: zigZagCurveCenter,
radius: zigZagCurveRadius,
startAngle: .pi,
endAngle: .pi,
clockwise: true)
}
path.addLine(to: CGPoint(x: bottomLeftCorner.x + radius, y: bottomLeftCorner.y))
path.addArc(withCenter: CGPoint(x: bottomLeftCorner.x + radius, y: bottomLeftCorner.y - radius),
radius: radius,
startAngle: .pi * 1.5,
endAngle: .pi,
clockwise: true)
path.close()
return path
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With