I'm going to suggest a somewhat different approach.
First, instead of adding a sublayer as the gradient layer, we'll make the custom view's layer itself a gradient layer:
private var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
// then, in init
// use self.layer as the gradient layer
gradientLayer = self.layer as? CAGradientLayer
We'll set the gradient animation to the full size of the view... that will give it a consistent width and speed.
Next, we'll add a subview as a mask, instead of a layer-mask. That will allow us to animate its width independently.
class GradProgressView: UIView {
@IBInspectable var color: UIColor = .gray {
didSet { setNeedsDisplay() }
}
@IBInspectable var gradientColor: UIColor = .white {
didSet { setNeedsDisplay() }
}
// this view will mask the percentage width
private let myMaskView = UIView()
// so we can calculate the new-progress-animation duration
private var curProgress: CGFloat = 0.0
public var progress: CGFloat = 0 {
didSet {
// calculate the change in progress
let changePercent = abs(curProgress - progress)
// if the change is 100% (i.e. from 0.0 to 1.0),
// we want the animation to take 1-second
// so, make the animation duration equal to
// 1-second * changePercent
let dur = changePercent * 1.0
// save the new progress
curProgress = progress
// calculate the new width of the mask view
var r = bounds
r.size.width *= progress
// animate the size of the mask-view
UIView.animate(withDuration: TimeInterval(dur), animations: {
self.myMaskView.frame = r
})
}
}
private var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// use self.layer as the gradient layer
gradientLayer = self.layer as? CAGradientLayer
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
gradientLayer.locations = [0.25, 0.5, 0.75]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-0.3, -0.15, 0]
animation.toValue = [1, 1.15, 1.3]
animation.duration = 1.5
animation.isRemovedOnCompletion = false
animation.repeatCount = Float.infinity
gradientLayer.add(animation, forKey: nil)
myMaskView.backgroundColor = .white
mask = myMaskView
}
override func layoutSubviews() {
super.layoutSubviews()
// if the mask view frame has not been set at all yet
if myMaskView.frame.height == 0 {
var r = bounds
r.size.width = 0.0
myMaskView.frame = r
}
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
layer.cornerRadius = bounds.height * 0.25
}
}
Here's a sample controller class - each tap will cycle through a list of sample progress percentages:
class ExampleViewController: UIViewController {
let progView = GradProgressView()
let infoLabel = UILabel()
var idx: Int = 0
let testVals: [CGFloat] = [
0.75, 0.3, 0.95, 0.25, 0.5, 1.0,
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
[infoLabel, progView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
infoLabel.textColor = .white
infoLabel.textAlignment = .center
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
progView.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
progView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
progView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
progView.heightAnchor.constraint(equalToConstant: 40.0),
infoLabel.topAnchor.constraint(equalTo: progView.bottomAnchor, constant: 8.0),
infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
])
progView.color = #colorLiteral(red: 0.9932278991, green: 0.5762576461, blue: 0.03188031539, alpha: 1)
progView.gradientColor = #colorLiteral(red: 1, green: 0.8578521609, blue: 0.3033572137, alpha: 1)
// add a tap gesture recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTap(_:)))
view.addGestureRecognizer(t)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
didTap(nil)
}
@objc func didTap(_ g: UITapGestureRecognizer?) -> Void {
let n = idx % testVals.count
progView.progress = testVals[n]
idx += 1
infoLabel.text = "Auslastung \(Int(testVals[n] * 100))%"
}
}