2

I have a progress method here:

func progress(incremented : CGFloat){
    if incremented <= self.bounds.width{
        self.progressLayer.removeFromSuperlayer()
        let originBezierPathProg = UIBezierPath(roundedRect: CGRect(x:0, y:0, width:0, height:self.bounds.height) , cornerRadius: self.viewCornerRadius)
        originBezierPathProg.close()

        let newBezierPathProg = UIBezierPath(roundedRect: CGRect(x:0, y:0, width:incremented, height:self.bounds.height) , cornerRadius: self.viewCornerRadius)
        bezierPathProg.close()

        self.progressLayer.path = originBezierPathProg.cgPath
        self.borderLayer.addSublayer(self.progressLayer)

        let animation = CABasicAnimation(keyPath: "path")
        animation.fromValue = originBezierPathProg.cgPath
        animation.toValue = newBezierPathProg.cgPath
        animation.duration = 1
        self.progressLayer.add(animation, forKey: animation.keyPath)

        self.progressLayer.path = newBezierPathProg.cgPath
    }
}

I am trying to make progress bar progress in an animated way. But when I call progress(100), it's simply rendering the bar without animation.

How can I fix it?

Update: Created MCVE as per Rob's suggestion: https://github.com/utkarsh2012/ProgressBarTest . I expect the progress bar to animate from width=0 to width=x (say 60)

Looks similar to this problem CABasicAnimation with CALayer path doesn't animate

zengr
  • 38,346
  • 37
  • 130
  • 192
  • While this [could be tightened up a bit](https://gist.github.com/robertmryan/eb8a9a422ad3820e3eb20fe143be3893), it works for me, as is. So the problem is either how you're calling it (too early, too frequently, from background thread), or your `bounds` or `incremented` values are such that the `if` statement is failing. We need producible example of the problem. – Rob May 13 '17 at 14:09
  • Let me come up with a simple project with this problem. Have been struggling with this for few hours now, I think the real problem is it's being called from a UIView's var ididSet (and probably too early?). I also do layout if needed after that. Looks at setupView: https://github.com/TwoPence/TwoPence/blob/milestone/TwoPence/MilestoneFutureView.swift – zengr May 13 '17 at 15:59
  • It's hard to tell. There's some unrelated stuff there, but, at the same time, not enough to reproduce the problem, methinks. If you want us to help, we really need [MCVE](http://stackoverflow.com/help/mcve), i.e. show us how to create blank project, insert the minimum amount of code that manifests your problem. Or, if you want to diagnose the problem yourself, add debugging code that checks (a) make sure it's happening on main thread; (b) that `bounds` is what you think it is when `progress(incremented:)` is called; (c) what `incremented` value was. – Rob May 13 '17 at 16:21
  • b and c are happening correctly. I am going to create a blank project and reproduce it. Thanks for the help! – zengr May 13 '17 at 16:37
  • @Rob created as simple xcode project (a UIView inside VC, same as my current project) and its reproducible https://github.com/utkarsh2012/ProgressBarTest – zengr May 13 '17 at 17:04

1 Answers1

3

The progress method originally shown in your question is fine. The problem is in how you used it. That having been said, let's go through the MCVE.

In your MCVE, there were a few things preventing the animation, namely:

  1. The progress method was as follows:

    func progress(incremented: CGFloat) {
        if incremented <= bounds.width {
            let toPath = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: incremented + 100, height: bounds.height), cornerRadius: viewCornerRadius)
    
            progressLayer.path = toPath.cgPath
            borderLayer.addSublayer(progressLayer)
    
            let animation = CABasicAnimation(keyPath: "path")
            animation.fromValue = self.progressLayer.path
            animation.toValue = toPath.cgPath
            animation.duration = 3
            progressLayer.add(animation, forKey: animation.keyPath)
        }
    }
    

    That is setting the path to be the new path, and then animating from that to the new path (i.e. animating to and from the same path). Thus, no animation.

    Save the old path before changing the path to its new "end state" and initiating the animation. Use that saved path as the fromValue of the animation.

  2. The value property was calling layoutSubviews:

    var value: Int? {
        didSet {
            progress(value: value!)
            self.layoutSubviews()
        }
    }
    

    Never call layoutSubviews() directly.

  3. The view controller was doing the following:

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        let view = DetailView(frame: self.view.frame)
        view.value = 60
        self.containerView.addSubview(view)
    }
    

    Don't try to start animations in viewDidLoad. That's too early in the view lifecycle. Use viewDidAppear (or whatever) instead. Doing this in viewDidLoad is generally too early in the view lifecycle.

  4. Related to that viewDidLoad code, you were changing the value of the DetailView (which triggered the animation) before you've added it to the view hierarchy.


See the fixes here https://github.com/robertmryan/ProgressBarTest.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Wow, thanks! So many things were going wrong! Thanks for much for the Github PR! Next time you are in SF, coffee on me :) – zengr May 13 '17 at 19:51
  • 1
    Or if you're in LA, look me up. – Rob May 13 '17 at 19:52
  • The animation worked but now it's causing a weird behavior where data changes from default values in labels to the actual values. http://imgur.com/a/zqn2o probably a followup question if I am not able to find a fix. – zengr May 13 '17 at 20:01
  • Yeah I noticed it looks weird. This is probably my 4th week in Swift, totally welcome feedback! I am going to look into how to make a better-animated progress bar. – zengr May 13 '17 at 20:32