Mask UIView with UIBezierPath - Stroke Only
A UIBezierPath
has several properties that only matter when stroking the path, including lineWidth
, lineCapStyle
, lineJoinStyle
, and miterLimit
.
A CGPath
has none of these properties.
Thus when you set mask.path = bezierPath.cgPath
, none of the stroke properties you set on bezierPath
carries over. You've extracted just the CGPath
from the UIBezierPath
and none of those other properties.
You need to set the stroking properties on the CAShapeLayer
rather than on any path object. Thus:
mask.path = bezierPath.cgPath
mask.lineWidth = 5
You also need to set a stroke color, because the default stroke color is nil, which means don't stroke at all:
mask.strokeColor = UIColor.white.cgColor
And, because you don't want the shape layer to fill the path, you need to set the fill color to nil:
mask.fillColor = nil
Exactly how to do it
with detailed discussion and explanation:
override func layoutSubviews() {
setup()
super.layoutSubviews()
}
private lazy var layerToUseAsAMask: CAShapeLayer = {
let l = CAShapeLayer()
// Recall that often you'd just draw the path here, but, somewhat confusingly
// we will draw the path in layout, since, we do need to know the sizes to
// draw the path
l.fillColor = UIColor.clear.cgColor
l.strokeColor = UIColor.white.cgColor
l.lineWidth = thick
l.lineCap = .round
// So, the bezier path is going to be on this layer. recall that (confusingly)
// it's the >layer<, not the path, which has the line qualities. The bezier
// path is a 100% platonic path!
// Recall too that overall, >all< of this is merely the CAShapeLayer, which
// will be >used as a mask< on our actual layer of interest ("ourLayer")
return l
}()
private lazy var ourLayer: CAGradientLayer = {
let l = CAGradientLayer()
layer.addSublayer(l)
l.frame = self.bounds
l.startPoint = CGPoint(x: 0.5, y: 0)
l.endPoint = CGPoint(x: 0.5, y: 1)
l.colors = [topColor.cgColor, bottomColor.cgColor]
// Recall that to round an image layer (or gradient later) you use the .mask
// facility (>not< a path)
l.mask = layerToUseAsAMask
// Recall too that, somewhat bizarrely really, changes at layout to a path
// push up to a layer (in our case our maskToUseAsMask); and indeed changes
// to a .mask facility (in our case, the one on our main layer) push up
// to a layer (ie, in our case to our main layer).
return l
}()
override func layoutSubviews() {
common()
super.layoutSubviews()
}
func common() {
ourLayer.frame = bounds
layerToUseAsAMask.frame = bounds
// Recall as explained above, every single time we layout, you have to
// actually draw the path again. It is "pushed up" to maskToUseAsMask
// and ultimately to the .mask on our ourLayer.
let pBar = UIBezierPath()
pBar.move(to: ...)
pBar.addLine(to: ...)
maskToUseAsMask.path = pBar.cgPath
layer.shadowOffset = CGSize(width: 5, height: 5)
layer.shadowColor = UIColor.green.cgColor
layer.shadowOpacity = 0.20
layer.shadowRadius = 2
}
In iOS a mask has been named by Apple .. a layer! Hence a "CAShapeLayer" can be a layer (a layer of an image), or in a totally different usage it can be ... a mask.
let someMaskIAmAMaking = CAMask() ... no!
let someMaskIAmAMaking = CAShapeLayer() ... strange but true. Bad Apple.
Always bear in mind that in iOS, "CAMask" does not exist - you use "CALayer" for both "CAMask" and "CALayer".
To shape a custom layer you have added (whether a gradient, photo of a cat, or anything else) you need to use a mask. That's a mask .. not a path.
We're going to be shaping a layer, so we'll use a mask. (And that means - WTF - a CAShapeLayer.)
let ourMask = CAShapeLayer( ...
Hence.
1. Make a layer, the only purpose of which is to use as a mask
Recall that often you'd just draw the path here, but, somewhat confusingly we will draw the path in layout, since, we do need to know the sizes to draw the path.
So, the bezier path is going to be on this layer. recall that (confusingly) it's the >layer<, not the path, which has the line qualities. the bezier path is a 100% platonic path!
Recall too that overall, >all< of this is merely the CAShapeLayer, which will be >used as a mask< on our actual layer of interest ("ourLayer")
2. Make your actual new special layer (here, a gradient)
Recall that to round an image layer (or gradient later) you use the .mask facility (>not< a path)
Recall too that, somewhat bizarrely really, changes at layout to a path push up to a layer (in our case our maskToUseAsMask); and indeed changes to a .mask facility (in our case, the one on our ourLayer) push up to a layer (ie, in our case to our ourLayer).
3. At layout times, actually draw the path
Recall as explained above, every single time we layout, you have to actually draw the path again. It is "pushed up" to maskToUseAsMask and ultimately to the .mask on our ourLayer.
4. Finally set an ordinary shadow "on the view"
So,
layer.shadowOpacity = 0.20 etc ...
Recall that the shadow "on the view" is the one built-in to ".layer", the "main and basic layer" of the view. You simply turn that on by setting the .shadowOpacity of .layer to a value.
That is quite different from adding a shadow on some other layer you have added. To add a shadow on some other layer you have added, such as catLayer, you use catLayer.path. (That procedure, which is totally different, is explained here for example: https://stackoverflow.com/a/57465440/294884 )