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
}
}
2
Answers
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 toTicketView
, 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 setshadowPath
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 ofTicketView
" approach to create that. Full code (things I’ve changed are addressed in the comments):As Sweeper said (+1), the problem is the mask on the
layer
.Personally, I would simplify this and just set the base layer of the view to be a
CAShapeLayer
, then you can set itspath
(and other properties) and then set theshadowPath
, and be done with it:I would make these properties mutable, but just have them trigger a layout if mutated:
Yielding:
Personally, I always make these
@IBDesignable
, so I can add them in Interface Builder and customize the parameters in the UI, rather than programmatically, but that is obviously a matter of personal preference. Regardless, here is my full implementation:You may also notice that I replaced all references to the
rect
parameter supplied to thedraw(_:)
method withbounds
.The above eliminates the need to implement
draw(_:)
at all, but if you do implement that, never userect
for constructing the path. Sometimes,draw(_:)
can be called whererect
is not the same as thebounds
. It may only be redrawing part of the view.Bottom line, in custom
draw(_:)
implementations, use therect
to determine what needs to be drawn, but never how it is drawn.