0

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?

Clay Ellis
  • 4,960
  • 2
  • 37
  • 45
  • Actually the transform matrix for parallel scale,transform matrix is applied at once which as a whole have the translate reflected in tx,ty and scale in a,b,c,d. So the animation block applies the end matrix which does everything smoothly as oppose to applying separately.Because each matrix is applied separately after applying the first one.My understanding :) – Muhammad Zohaib Ehsan Nov 30 '17 at 07:50
  • Log the transform of the button after completion, in both cases. You'll see they're different. Check out [this post](https://stackoverflow.com/questions/1111277/applying-multiple-transforms-to-a-uiview-calayer) – n00bProgrammer Nov 30 '17 at 07:53
  • @MuhammadZohaibEhsan, what you're saying then is that the animation engine can only animate a single update to the transformation matrix (with any number of rotations, scales, or translations) smoothly at a time? – Clay Ellis Dec 05 '17 at 18:30
  • @n00bProgrammer that's exactly what I had done (see the end of the post). And their end states are the exact same in both the parallelized and sequential animations. The sequential animation takes one extra step to get there. – Clay Ellis Dec 05 '17 at 18:32

1 Answers1

2

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.

Malik
  • 3,763
  • 1
  • 22
  • 35
  • Right, my question is, why does scaling after the translation completes cause a jump before scaling? I'm just scaling something that's already been translated. Logging the transforms before and after the animations shows that that's the case. (Sorry it took so long to get back to you, holidays.) – Clay Ellis Dec 05 '17 at 18:25
  • 1
    You might need to look into anchor point and its effect on transformations. You can find plenty of resources online that explain it in great detail. For a quick solution, before your scale transformation, you can set your label's layer's anchor point to (0,0) if you want it to scale towards right and down or (1,1) to scale it towards left and up – Malik Dec 06 '17 at 00:58