SwiftyJD
SwiftyJD

Reputation: 5461

UIView Animation not being called after animation moves back to original location

Currently I'm using a button to activate an UIView Animation and in it's completion block I'm using a CABasicAnimation to return it to it's starting location. The animation runs smoothly at first and returns to its original starting point but when I press the button again the animation automatically starts at the spot the original UIView Animation ended whereas it's suppose to start at the original starting point.

class ObjectCreateMoveViewController: UIViewController, CAAnimationDelegate {

  let square = UIView()

  override func viewDidLoad() {
    super.viewDidLoad()

    createSquare()

  }

  func createSquare() {

    square.frame =  CGRect(x: 10, y: 10, width: 100, height: 100)
    square.backgroundColor = UIColor.red
    square.layer.borderColor = UIColor.blue.cgColor
    square.layer.borderWidth = 4
    square.layer.cornerRadius = 2
    view.addSubview(square)
  }


  func transformAnimation () {

    let transAnimation = CABasicAnimation(keyPath: "transform.translation")
    transAnimation.toValue = NSValue(cgPoint: CGPoint(x: 10, y: 10))

    let rotAnimation = CABasicAnimation(keyPath: "transform.rotation")
    rotAnimation.toValue = -CGFloat.pi

    let scaleAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
    scaleAnimation.toValue = NSNumber(value: 1)

    let groupTransform = CAAnimationGroup()
    groupTransform.duration = 3
    groupTransform.delegate = self
    groupTransform.beginTime = CACurrentMediaTime()
    groupTransform.animations = [transAnimation, rotAnimation, scaleAnimation]
    groupTransform.isRemovedOnCompletion = false
    groupTransform.fillMode = kCAFillModeForwards
    groupTransform.setValue("circle", forKey: "transform")
    square.layer.add(groupTransform, forKey: "transform")
  }

  func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    if flag {
      //should be (10,10)but when I printed out the ending positions it was (60,60)
      square.layer.position.x = 60
      square.layer.position.y = 60
      print("This is the x point \(square.layer.position.x)")
      print("This is the y point \(square.layer.position.y)")
    }
  }

  @IBAction func transformButtonPressed(_ sender: Any) {

    UIView.animate(withDuration: 3, delay: 0, options: .curveEaseInOut, animations: {

      let translateTransform = CGAffineTransform(translationX: 150, y: 200)
      self.square.transform = translateTransform.scaledBy(x: 1.5, y: 1.5).rotated(by: CGFloat.pi/2)

    }, completion: { _ in

      self.transformAnimation()

    })
  }
}

Upvotes: 1

Views: 1227

Answers (2)

DonMag
DonMag

Reputation: 77691

David Rönnqvist gave you a really good description to work from, but if you want a really, really simple option for "animating it back the way it came" ...

Option 1 - use .autoReverse:

@IBAction func transformButtonPressed(_ sender: Any) {

    UIView.animate(withDuration: 3, delay: 0, options: [.curveEaseInOut, .autoreverse], animations: {

        let translateTransform = CGAffineTransform(translationX: 150, y: 200)
        self.square.transform = translateTransform.scaledBy(x: 1.5, y: 1.5).rotated(by: CGFloat.pi/2)

    }, completion: { _ in

        self.square.transform = .identity

    })
}

Option 2 - if you want to "send it back" on another action, for example:

@IBAction func transformButtonPressed(_ sender: Any) {

    UIView.animate(withDuration: 3, delay: 0, options: .curveEaseInOut, animations: {

        let translateTransform = CGAffineTransform(translationX: 150, y: 200)
        self.square.transform = translateTransform.scaledBy(x: 1.5, y: 1.5).rotated(by: CGFloat.pi/2)

    }, completion: { _ in

        // un-comment to animate it back right away,
        // or leave commented, and call transformBack() from somewhere else
        //self.transformBack()

    })
}

func transformBack() -> Void {

    UIView.animate(withDuration: 3, delay: 0, options: .curveEaseInOut, animations: {

        // resets transform to "none"
        self.square.transform = .identity

    }, completion: { _ in

    })

}

Upvotes: 1

David Rönnqvist
David Rönnqvist

Reputation: 56635

There's a few things going on here, so let me first give you some background and then go through what is happening to see where things go wrong.

Spoiler: It's the combination of isRemovedOnCompletion = false and a forwards fillMode.


Some background

On iOS, every view always have a layer that it owns and manages. Setting a property like the frame or the transform of the view automatically sets the frame or transform of the layer. For the layer, this value that is set is sometimes called the "model value" to distinguish it from the "presentation value" which is how the layer appears on screen during an ongoing animation. In practice this means two things:

  1. If one tries to inspect the value of the layer's position while the view is animating using UIView animations, then the position value is going to be the end value, that was assigned in the animation block. To get to the "current" value as it appears on screen, one would have to look at the layer's presentation layer's value.

  2. If one adds an explicit CAAnimation to a layer, the layer is going to animate the change on screen, but if the animated property is inspected during the animation it's going to remain unchanged. Once the animation finishes and is removed, the layer is once again going to render it's model value, making it appear as if it jumped back to an old value unless the value was also changed.

What's happening in this case

Starting out

The view (and its layer) stars out with a frame origin of (10, 10) and a size of (100, 100) making it's center (the layer's position) (60, 60). Since no transform is set, the "identity transform" (no transformation) is used.

Triggering the animation

When the button is pressed, the animation block is executed which changes the transform of the view (and thus also its layer) to one with a translation of (150, 200) a scale of (1.5, 1.5) and a rotation of π/2. This changes the transform "model" value of the layer.

Running the completion block

When the first animation is completed, it triggers the second animation. At this point the model values are still what they were set to in the animation block.

Three transform animations is configures to animate to a translation of (10, 10), a rotation of , and a scale of 1. The group of these animations is configured to not be removed upon completion. The model values are not updated at this point.

The animation group completes

The animation group finishes but is not removed. At this point the model values are still that transform that was set in the original animation block. Not removing the CAAnimation has persisted a difference between the model (what the property is) and the presentation (how it appears on screen).

Note that the position is expected to be (60, 60) at this point, because it was never changed. Changing the transform property doesn't change the position property, only where the layer appears on screen.

The first animation is triggered again

The next time the button is pressed the first animation is triggered again. It is going to animate from the "old" value to the "new" value. Since the transform property (of the model) was never changed, these are the same.

I can't recall if it's documented or not, but UIView animations use the property that was changed as the key when adding animations to the layer (which happens behind the scenes).

Because the second animation was added to the layer for the key "transform" and the layer can only have one animation per unique key. The group animation is removed. This makes it look like the view jumps back to the "end value" of the first animation.

As far as I can recall what UIKit is doing, the view would then perform a 3 second animation from one value to the same vale (i.e. not changing anything).

Once the UIView animation finishes, the completion block runs the second animation again which adds the group animation to the layer.

How to fix it

The real problem are these two lines:

groupTransform.isRemovedOnCompletion = false
groupTransform.fillMode = kCAFillModeForwards

Together they seem like they do the right thing because it appears correct on screen, but as you've seen above they persist the temporary difference between the model and the presentation layer long after the animation completes.

As a good rule of thumb: the "only" time to not remove an animation upon completion is if it's animating the disappearance of a view/layer and the view/layer is being removed upon completion.


One way to approach the fix would be to remove those two lines, and then deal with having to also update the view's (or layer's) transform to the new transform value before adding the animation group to the layer.

However, in this case the same animation could be achieved using another UIView animation from within the completion block. Alternatively you could use a UIView key-frame animation with two 3 second steps.

Sticking with UIView animations where possible has the benefit that the model values are updated.

Upvotes: 2

Related Questions