The issue is that your path is a rounded rectangle. In the image you shared with us, it’s probably about 2-3% stroked. Change it to stroke 90% of the way, and you’ll see it trying to draw a wide and extremely short rounded rectangle, e.g.:
Instead, just make the path a line, and it will work as intended:
let path = UIBezierPath()
let bounds = topView.bounds
path.move(to: CGPoint(x: bounds.minX + bounds.width * 0.15, y: bounds.minY + bounds.height / 1.5))
path.addLine(to: CGPoint(x: bounds.minX + bounds.width * 0.85, y: bounds.minY + bounds.height / 1.5))
You might also want to round the caps of your shape layers:
trackLayer.lineCap = .round // or whatever you want
shapeLayer.lineCap = .round
And, of course, this change lost the 2 point height of your original path, so if you want to make these shape layer’s thicker, just increase their respective lineWidth
values.
A couple of unrelated observations:
viewDidLayoutSubviews()
and viewDidAppear(_:)
should call their super
implementations.
viewDidLayoutSubviews()
can be called multiple times, so you don’t want to instantiate a new trackLayer
every time. Or if you do, make sure to remove the prior one.
When adding subviews/sublayers, it is prudent to use bounds
instead of frame
. In this case it probably doesn’t matter, but in some cases you can get all sorts of weird problems because frame
is in the view’s superview’s coordinate system, whereas bounds
is the coordinate system of the view in question.
Personally, if you were to keep this code in the view controller, I’d suggest:
- add the shape layers and gradient in
viewDidLoad
;
- update the paths and the bounds of the gradient from
viewDidLayoutSubviews
;
- I’d put these various shape layer and gradient methods in their own private extension.
Even better, all of this animation code doesn’t really belong in the app’s view controller at all, but rather a UIView
subclass (or a child view controller).
Thus, perhaps:
@IBDesignable
public class GradientProgressView: UIView {
private var shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = .round
return shapeLayer
}()
private var trackLayer: CAShapeLayer = {
let trackLayer = CAShapeLayer()
trackLayer.strokeColor = UIColor.groupTableViewBackground.cgColor
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = .round
return trackLayer
}()
private var gradient: CAGradientLayer = {
let gradient = CAGradientLayer()
let color = UIColor(red: 11/255, green: 95/255, blue: 244/255, alpha: 1).cgColor
let sndColor = UIColor(red: 255/255, green: 87/255, blue: 87/255, alpha: 1).cgColor
gradient.colors = [color, sndColor]
gradient.locations = [0.0, 1.0]
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 0)
return gradient
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
addSubLayers()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addSubLayers()
}
override public func layoutSubviews() {
super.layoutSubviews()
updatePaths()
}
override public func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
setProgress(0.75, animated: false)
}
public func setProgress(_ progress: CGFloat, animated: Bool = true) {
if animated {
animateStroke(to: progress)
} else {
shapeLayer.strokeEnd = progress
}
}
}
private extension GradientProgressView {
func addSubLayers() {
layer.addSublayer(trackLayer)
layer.addSublayer(shapeLayer)
layer.addSublayer(gradient)
}
func updatePaths() {
let lineWidth = bounds.height / 2
trackLayer.lineWidth = lineWidth * 0.75
shapeLayer.lineWidth = lineWidth
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.minX + lineWidth / 2, y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.maxX - lineWidth / 2, y: bounds.midY))
trackLayer.path = path.cgPath
shapeLayer.path = path.cgPath
gradient.frame = bounds
gradient.mask = shapeLayer
}
func animateStroke(to progress: CGFloat) {
let key = "lineStrokeAnimation"
layer.removeAnimation(forKey: key)
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = progress
basicAnimation.duration = 1.5
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
basicAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
shapeLayer.add(basicAnimation, forKey: key)
}
}
Then the view controller is merely:
class ViewController: UIViewController {
@IBOutlet weak var gradientProgressView: GradientProgressView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateProgress()
}
...
}
// MARK: - Progress related methods
private extension ViewController {
func updateProgress() {
let distance = currLeasingCar!.currentKm - currLeasing!.startKm
let value = CGFloat(distance) / CGFloat(finalKm)
gradientProgressView.setProgress(value)
}
}
viewDidLayoutSubviews()
, where should thedisplayLine()
be called instead? – Simonsimona