Reputation: 4043
I have a CAShapeLayer()
with a gradient on top which is being animated, but somehow it looks like in the image below:
How come it looks like this?
My code:
override func viewDidLayoutSubviews() {
displayLine()
}
override func viewDidAppear(_ animated: Bool) {
animateStroke()
}
func displayLine() {
let trackLayer = CAShapeLayer()
let rect = CGRect(x: topView.frame.width * 0.15, y: topView.frame.size.height / 1.5, width: topView.frame.width * 0.7, height: 2)
let path = UIBezierPath(roundedRect: rect, cornerRadius: 1)
trackLayer.path = path.cgPath
trackLayer.strokeColor = UIColor.groupTableViewBackground.cgColor
trackLayer.lineWidth = 3
trackLayer.fillColor = UIColor.clear.cgColor
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 4
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeEnd = 0
topView.layer.addSublayer(trackLayer)
topView.layer.addSublayer(shapeLayer)
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)
gradient.frame = topView.bounds
gradient.mask = shapeLayer
topView.layer.addSublayer(gradient)
}
func animateStroke() {
if !animated {
animated = true
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
var value: Double?
let distance = currLeasingCar!.currentKm - currLeasing!.startKm
value = Double(distance) / Double(finalKm)
basicAnimation.toValue = value
basicAnimation.duration = 1.5
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
basicAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
shapeLayer.add(basicAnimation, forKey: "lineStrokeAnimation")
}
}
Upvotes: 2
Views: 488
Reputation: 438467
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:
viewDidLoad
;viewDidLayoutSubviews
; 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)
}
}
Upvotes: 5