A Story of a Animation Trap

The great thing about the “stay home” policy is that I literally can “waste” my time playing the UIKit during the weekend. 💻
Last weekend, I found an interesting case, which is actually a basic concept of UIKit, but I was shamefully trapped. 🕳️

Imagine (or just open up your playground) that we have a view aView.
We want to move the view 20 points horizontally, with animation.
Here’s how we do it:

aView.frame = zeroOriginFrame // Set the view to (0, 0, 50, 100)
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1, delay: 2, animations: {
    aView.transform = CGAffineTransform.identity.translatedBy(x: 20, y: 0)
})

We are doing great like a boss, the view is moved after 2 seconds delay.

One day, someone came (yeah that was me), and move the first line from top to the bottom:

UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1, delay: 2, animations: {
    aView.transform = CGAffineTransform.identity.translatedBy(x: 20, y: 0)
})
aView.frame = zeroOriginFrame // Moved

Looks like it does no harm. The view will be moved to the (0, 0) firstly, and then be moved right after 2 seconds. Let’s check how correct we are:

Wow,  shouldn't the view start from the (0, 0)? I did set a 2-second delay. So it should be like: 1. set the frame to zero 2. animation. Unfortunately, no.
Let’s see how it works in an old fashion way.

UIView.beginAnimations(“transformAnimation”, context: nil)
UIView.setAnimationDuration(1)
UIView.setAnimationDelay(2)
aView.transform = CGAffineTransform.identity.translatedBy(x: 20, y: 0)
UIView.commitAnimations()

aView.frame = zeroOriginFrame

This is exactly the same thing as the UIViewPropertyAnimator.runningPropertyAnimator, just we re-write it in the deprecated API.

This is quite clear, the UIView.setTransform: is executed before the UIView.setFrame:. The animation block makes sure the property inside the block won’t be executed in the next transaction, but those properties have been assigned to the view. So the next  UIView.setFrame: was setting a view which has been “transformed”.

But we are not satisfied yet! Why is the starting position of the aView in nowhere? I didn’t set any minus x coordinate!
Here’s the suspect: the anchor position.
The anchor position of the aView before setting the frame was  CGPoint (25 50). But after calling the UIView.setFrame:, the anchor position becomes: CGPoint (5 50).
You set the frame, but the "transform" has been there for a century. The system adjusts the anchor position to fit the requirements of: 1. Keep the animation state and the "transform" 2. set the frame. This leads to undefined behavior.

So what’s the takeaway? 1. Make sure your change of the layout doesn’t involve a global state like animation. 2. The delay of animation makes the inferred transaction implicit, try to avoid it by using something like dispatch queue.

Afternote: I couldn’t find the corresponding symbol of changing the anchor position by setting the frame in the animation context, please let me know if you find something interesting.

Show Comments

Get the latest posts delivered right to your inbox.