skip to Main Content

I made a custom progressbar for my app (following an article on medium), it works as intended but i have one problem, when i change the progress value then it jumps to fast! (dont get confused by the percent values below the bar, they are off, i know that)

enter image description here

i use setNeedsDisplay() to redraw my view.

I want the bar to animate smoothly, so in my case a bit slower.

this is the draw function of the bar:

 override func draw(_ rect: CGRect) {
    backgroundMask.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.height * 0.25).cgPath
    layer.mask = backgroundMask

    let progressRect = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))

    progressLayer.frame = progressRect
    progressLayer.backgroundColor = UIColor.black.cgColor

    gradientLayer.frame = rect
    gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
    gradientLayer.endPoint = CGPoint(x: progress, y: 0.5)
}

Here is the whole Class i used:

https://bitbucket.org/mariwi/custom-animated-progress-bars-with-uibezierpaths/src/master/ProgressBars/Bars/GradientHorizontalProgressBar.swift

Anyone with an idea?

EDIT 1:
Similar questions helped, but the result is not working properly.
I aded this function to set the progress of the bar:

func setProgress(to percent : CGFloat)
{
    progress = percent
    print(percent)
    
    let rect = self.bounds
    let oldBounds = progressLayer.bounds
    let newBounds = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
    
    
    let redrawAnimation = CABasicAnimation(keyPath: "bounds")
    redrawAnimation.fromValue = oldBounds
    redrawAnimation.toValue = newBounds

    redrawAnimation.fillMode = .forwards
    redrawAnimation.isRemovedOnCompletion = false
    redrawAnimation.duration = 0.5
    
    progressLayer.bounds = newBounds
    gradientLayer.endPoint = CGPoint(x: progress, y: 0.5)
    
    progressLayer.add(redrawAnimation, forKey: "redrawAnim")

    
}

And now the bar behaves like this:
enter image description here

2

Answers


  1. Chosen as BEST ANSWER

    After digging a while and a ton of testing, i came up with a solution, that suited my needs! Altough the above answer from DonMag was also working great (thanks for your effort), i wanted to fix what halfway worked. So the problem was, that the bar resized itself from the middle of the view. And on top, the position was also off for some reason.

    First i set the position back to (0,0) so that the view started at the beginning (where it should). The next thing was the resizing from the middle, because with the position set back, the bar only animated to the half when i set it to 100%. After some tinkering and reading i found out, that changing the anchorPoint of the view would solve my problem. The default value was (0.5,0.5), changing it into (0,0) meant that it would only expand the desired direction.

    After that i only needed to re-set the end of the gradient, so that the animation stays consistent between the different values. After all of this my bar worked like I imagined. And here is the result:

    enter image description here

    Here is the final code, i used to accomplish this:

    func setProgress(to percent : CGFloat)
    {
        progress = percent
        print(percent)
        let duration = 0.5
        
        let rect = self.bounds
        let oldBounds = progressLayer.bounds
        let newBounds = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
        
        
        let redrawAnimation = CABasicAnimation(keyPath: "bounds")
        redrawAnimation.fromValue = oldBounds
        redrawAnimation.toValue = newBounds
    
        redrawAnimation.fillMode = .both
        redrawAnimation.isRemovedOnCompletion = false
        redrawAnimation.duration = duration
        
        progressLayer.bounds = newBounds
        progressLayer.position = CGPoint(x: 0, y: 0)
        
        progressLayer.anchorPoint = CGPoint(x: 0, y: 0)
        
        progressLayer.add(redrawAnimation, forKey: "redrawAnim")
        
        let oldGradEnd = gradientLayer.endPoint
        let newGradEnd = CGPoint(x: progress, y: 0.5)
        let gradientEndAnimation = CABasicAnimation(keyPath: "endPoint")
        gradientEndAnimation.fromValue = oldGradEnd
        gradientEndAnimation.toValue = newGradEnd
        
        gradientEndAnimation.fillMode = .both
        gradientEndAnimation.isRemovedOnCompletion = false
        gradientEndAnimation.duration = duration
        
        gradientLayer.endPoint = newGradEnd
        
        gradientLayer.add(gradientEndAnimation, forKey: "gradEndAnim")
        
    }
    

  2. 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))%"
        }
        
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search