Why does a CGAffineTransform applied in steps behave differently than applied at once?

596 Views Asked by At

I'm seeing some unexpected, inconsistent behavior when applying transforms in steps as opposed to applying them at once and I'd like to know why.

Say we have a label that we'd like to translate to the right 100 and down 50 and then scale up to 1.5 times the original size. So there are two transformations:

  1. Translation
  2. Scale

And say that we are experimenting with two different animations:

  1. Perform the translation and scale in parallel
  2. Perform the translation, then perform the scale in sequence

In the first animation you might do something like this:

UIView.animate(withDuration: 5, animations: {
    label.transform = label.transform.translatedBy(x: 100, y: 50).scaledBy(x: 1.5, y: 1.5)
}, completion: nil)

First Animation

And everything behaves how you'd expect. The label translates and scales smoothly at the same time.

In the second animation:

UIView.animate(withDuration: 5, animations: {
    label.transform = label.transform.translatedBy(x: 100, y: 50)
}, completion: { _ in
    UIView.animate(withDuration: 5, animations: {
        label.transform = label.transform.scaledBy(x: 1.5, y: 1.5)
    }, completion: nil)
})

Animation 2

The label translates correctly, and then boom, it jumps unexpectedly and then starts to scale.

What causes that sudden, unexpected jump? From inspecting the matrices for each transform (the parallelized and the sequential transforms) the values are the same, as would be expected.

Parallelized Animation

transform before translate and scale: CGAffineTransform(a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: 0.0, ty: 0.0)
translate and scale transform: CGAffineTransform(a: 1.5, b: 0.0, c: 0.0, d: 1.5, tx: 100.0, ty: 50.0)
transform after translate and scale: CGAffineTransform(a: 1.5, b: 0.0, c: 0.0, d: 1.5, tx: 100.0, ty: 50.0)

Sequential Animation

transform before translation: CGAffineTransform(a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: 0.0, ty: 0.0)
translation transform: CGAffineTransform(a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: 100.0, ty: 50.0)
transform after translation: CGAffineTransform(a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: 100.0, ty: 50.0)

transform before scale: CGAffineTransform(a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: 100.0, ty: 50.0)
scale transform: CGAffineTransform(a: 1.5, b: 0.0, c: 0.0, d: 1.5, tx: 100.0, ty: 50.0)
transform after scale: CGAffineTransform(a: 1.5, b: 0.0, c: 0.0, d: 1.5, tx: 100.0, ty: 50.0)

So what is it that causes the sudden jump?

1

There are 1 best solutions below

2
On

You need to understand how animation works in iOS. Your animation closure block runs right away and the final values are assigned to the object straight away (This is one of the most important points that a lot of people forget). All the animation block does is that it makes it appear to take that much time. Let me elaborate with an example.

let x = UIView()
x.alpha = 0
//At this point, alpha is 0
UIView.animate(withDuration: 5, animations: {
    x.alpha = 1
}, completion: nil)
//At this point, alpha is 1 right away. But the animation itself will take 5 seconds

With that in mind, let's look at the second example that you posted

UIView.animate(withDuration: 5, animations: {
    label.transform = label.transform.translatedBy(x: 100, y: 50)
}, completion: { _ in
    UIView.animate(withDuration: 5, animations: {
        label.transform = label.transform.scaledBy(x: 1.5, y: 1.5)
    }, completion: nil)
})

The first animation runs and translates your view right away. It only takes 5 seconds to move there but your view's x & y values have already changed. Upon completion, you scale it resulting in a weird behaviour.