The short answer to your problem is most likely that you need to call cgPath.copy(strokingWithWidth: 3.0, lineCap: .butt, lineJoin: .bevel, miterLimit: 1.0)
to convert your stroking path on mask to fill path.
But to break this down step by step...
First, you are drawing a gradient, which is masked by a line. Not drawing a line that has applied gradient. So only one layer needs to be added and it needs to be a gradient layer. So a first step should draw a rectangle with a gradient:
func drawLine(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
self.layer.addSublayer(gradientLayer)
}
Now that gradient is visible lets apply some mask to it. A circle should be an easy task:
func drawLine(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
layer.path = UIBezierPath(ovalIn: path.bounds).cgPath
return layer
}()
self.layer.addSublayer(gradientLayer)
}
if you do this and you used some non-zero starting coordinates of the path then you will see that the circle is cut off. The path that is being masked must be in gradient layer coordinates so path.bounds
needs to be corrected to
gradientLayer.mask = {
let layer = CAShapeLayer()
layer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: path.bounds.width, height: path.bounds.height)).cgPath
return layer
}()
now that coordinates are OK let's convert this to a line. It gets a little bit more complicated because we need to subtract the origin of original path:
func drawLine(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
let lineStart = CGPoint(x: start.x - path.bounds.minX,
y: start.y - path.bounds.minY)
let lineEnd = CGPoint(x: end.x - path.bounds.minX,
y: end.y - path.bounds.minY)
let path = UIBezierPath()
path.move(to: lineStart)
path.addLine(to: lineEnd)
layer.path = path.cgPath
return layer
}()
self.layer.addSublayer(gradientLayer)
}
there are other ways to move the path. A transform could be used just as well. So if this approach is not nice enough for you please choose whichever you wish.
However, this now draws nothing at all. And the reason is that when using mask
a fill
approach is always used. And filling a line does nothing as it has no area. You could use multiple lines to draw a custom shape for instance. And in your case you could use 4 lines which would represent a rectangle which is the line you expect to draw.
However, drawing lines is a pain so thankfully there is a not-very-intuitively designed API which converts your stroked path to a fill path. So in your case it converts a line into a rectangle with a defined width (and other properties which are only needed for more complex strokes). You can use
public func copy(strokingWithWidth lineWidth: CGFloat, lineCap: CGLineCap, lineJoin: CGLineJoin, miterLimit: CGFloat, transform: CGAffineTransform = .identity) -> CGPath
The code is not documented but in online documentation you may find
Discussion The new path is created so that filling the new path draws
the same pixels as stroking the original path with the specified line
style.
Now the end result should look something like this:
func drawLine(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
let lineStart = CGPoint(x: start.x - path.bounds.minX,
y: start.y - path.bounds.minY)
let lineEnd = CGPoint(x: end.x - path.bounds.minX,
y: end.y - path.bounds.minY)
let path = UIBezierPath()
path.move(to: lineStart)
path.addLine(to: lineEnd)
layer.path = path.cgPath.copy(strokingWithWidth: 3.0, lineCap: .butt, lineJoin: .bevel, miterLimit: 1.0)
return layer
}()
self.layer.addSublayer(gradientLayer)
}
I hardcoded line width to 3.0
but you are probably best adding this as a method parameter or as view property.
To play around with different setup and see results quickly from this answer you can create a new project and replace your view controller code to the following:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
view.addSubview({
let label = UILabel(frame: CGRect(x: 20.0, y: 70.0, width: 300.0, height: 20.0))
label.text = "Tap to toggle through drawing types"
return label
}())
}
private var instance: Int = 0
private var currentView: UIView?
@objc private func onTap() {
currentView?.removeFromSuperview()
let view = MyView(frame: CGRect(x: 30.0, y: 100.0, width: 100.0, height: 200.0))
currentView = view
view.backgroundColor = .lightGray
self.view.addSubview(view)
view.drawLine(type: instance, start: CGPoint(x: 90.0, y: 10.0), toPoint: .init(x: 10.0, y: 150.0))
instance += 1
}
}
private extension ViewController {
class MyView: UIView {
func drawLine(type: Int, start: CGPoint, toPoint end: CGPoint) {
let methods = [drawLine1, drawLine2, drawLine3, drawLine4, drawLine5]
methods[type%methods.count](start, end)
}
private func drawLine1(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
self.layer.addSublayer(gradientLayer)
}
private func drawLine2(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
layer.path = UIBezierPath(ovalIn: path.bounds).cgPath
return layer
}()
self.layer.addSublayer(gradientLayer)
}
private func drawLine3(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
layer.path = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: path.bounds.width, height: path.bounds.height)).cgPath
return layer
}()
self.layer.addSublayer(gradientLayer)
}
private func drawLine4(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
let lineStart = CGPoint(x: start.x - path.bounds.minX,
y: start.y - path.bounds.minY)
let lineEnd = CGPoint(x: end.x - path.bounds.minX,
y: end.y - path.bounds.minY)
let path = UIBezierPath()
path.move(to: lineStart)
path.addLine(to: lineEnd)
layer.path = path.cgPath
return layer
}()
self.layer.addSublayer(gradientLayer)
}
private func drawLine5(start: CGPoint, toPoint end: CGPoint) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = path.bounds
gradientLayer.colors = [UIColor.white.cgColor, UIColor.red.cgColor]
gradientLayer.mask = {
let layer = CAShapeLayer()
let lineStart = CGPoint(x: start.x - path.bounds.minX,
y: start.y - path.bounds.minY)
let lineEnd = CGPoint(x: end.x - path.bounds.minX,
y: end.y - path.bounds.minY)
let path = UIBezierPath()
path.move(to: lineStart)
path.addLine(to: lineEnd)
layer.path = path.cgPath.copy(strokingWithWidth: 3.0, lineCap: .butt, lineJoin: .bevel, miterLimit: 1.0)
return layer
}()
self.layer.addSublayer(gradientLayer)
}
}
}