4

I have a very simple animation block which performs the following animation:

enter image description here
(tap the image if the animation doesn't play)

func didTapButton() {
  // reset the animation to 0
  centerYConstraint.constant = 0
  superview!.layoutIfNeeded()

  UIView.animate(
    withDuration: 1,
    animations: {
      // animate the view downwards 30 points
      self.centerYConstraint.constant = 30
      self.superview!.layoutIfNeeded()
  })
}

Everything is great when I play the animation by itself. It resets to position 0, then animates 30 points.

The problem is when the user taps the button multiple times quickly (i.e. during the middle of an ongoing animation). Each time the user taps the button, I would expect it to reset to position 0, then animate downwards 30 points. Instead, I get this behavior:

enter image description here
(tap the image if the animation doesn't play)

It's clearly traveling well over 30 points. (Closer to 120 points.)

Why is this happening, and how can I "reset" the animation properly so that it only at most travels 30 points?

Things that I have tried that didn't work:

  • Using options: UIViewAnimationOptions.beginFromCurrentState. It does the same exact behavior.
  • Instead of executing the animation directly, do it after a few milliseconds using dispatch_after. Same behavior.
  • "Canceling" the previous animation by using a 0 second animation that changes the constant to 0, and calls superview!.layoutIfNeeded.

Other notes:

  • If instead of changing the position, I change the height via a constraint, I get similar odd behavior. E.g. if I set it to go from 30 points to 15 points, when repeatedly pressing the button, the view will clearly grow up to around 120 points.
  • Even if I don't use constraints, and instead I animate the transform from CGAffineTransform.identity to CGAffineTransform(scaleX: 0.5, y: 0.5), I get the same behavior where it'll grow to 120 points.
  • If I try a completely different animatable property say backgroundColor = UIColor(white: 0, alpha: 0.5) to backgroundColor = UIColor(white: 0, alpha: 0), then I get correct behavior (i.e. each time I tap the button, the color resets to 0.5 gray at most. It never gets to black.
Senseful
  • 86,719
  • 67
  • 308
  • 465
  • What behavior do you want? Do you really want it to stop the current animation, jump back to the original position, and then animate down 30 points again? Or do you want it to just ignore the second and subsequent taps)? – Rob May 24 '17 at 07:00
  • 1
    @Rob in this case I **want it** to jump back to 0 position, and animate down to 30 points. – Senseful May 24 '17 at 07:02

2 Answers2

3

I can reproduce the behavior you describe. But if I call removeAllAnimations() on the layer of the view that is moving, the problem goes away.

@IBAction func didTapButton(_ sender: Any) {
    animatedView.layer.removeAllAnimations()

    centerYConstraint.constant = 0
    view.layoutIfNeeded()

    centerYConstraint.constant = 30
    UIView.animate(withDuration: 2) {
        self.view.layoutIfNeeded()
    }
}

Note, I'm not removing the animation from the superview (because that still manifests the behavior you describe), but rather of the view that is moving.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • For more information on removing/canceling animations: https://stackoverflow.com/questions/9569943/how-to-cancel-uiview-block-based-animation – Senseful May 24 '17 at 18:36
1

So far, the only solution I found is to recreate the view rather than trying to reset the existing one.

This gives me the exact behavior I was looking for:

enter image description here
(tap the image if the animation doesn't play)

It's unfortunate that you need to remove the old view and create a new one, but that's the only workaround I have found.

Senseful
  • 86,719
  • 67
  • 308
  • 465