First steps with UIKit animations

Roberto Sampaio
My iOS Studies
Published in
6 min readMay 2, 2021

In the last study, we learned how to configure an UITableViewDiffableDataSource. If you don't want to miss these studies, you can check them here. Also, you can check the base project in this post and the code on github. We will use the branches study02-start and study02-final for start and final of all the implementation.

We will learn how to make UIKit animations in this study. I created the ones below in the project:

The first animation is using UIView.animate function and the second is using UIViewController transitions with UIView.animateKeyFrame function.

Where should we start?

First, we need to understand how UIView animations works. Let's take a look in the UIView.animate function.

open class func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void)

The animate function asks only for the duration and a closure with all the animations we want to perform. Open a Playground file on your Xcode with File > New > Playground. Let's add some base code to visualize how it works.

import UIKit
import PlaygroundSupport
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))PlaygroundPage.current.liveView = containerViewlet view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = .green
containerView.addSubview(view)

If you run your Playground, you will see a white square containing a green square. We want to animate the green square to move inside the white one.

In order to animate it, we need to call the animate function updating the frame. Add this piece of code at the bottom.

UIView.animate(withDuration: 1) {
view.frame = CGRect(x: 0, y: 100, width: 200, height: 200)
}

The green square moved half way to the bottom.

If we want to change the size, we can change width and height.

Change the last piece of code to the code below:

UIView.animate(withDuration: 1) {
view.frame = CGRect(x: 0, y: 100, width: 100, height: 250)
}

Now the green square changed its size.

I think you already got how it works! To animate something you need to call animate and pass the duration and a closure where you will update the properties you want to change. The properties will be updated gradually over the duration time.

Which properties are animatable?

The animatable properties can be categorized in:

Position and size

  • bounds
  • frame
  • center

Transformation

  • rotation
  • scale
  • translation

Appearance

  • backgroundColor
  • alpha

We already updated theframe, a position property. Let's learn how to apply transformations.

Transformations are applied by a struct called CGAffineTransform. To apply a 45 degrees rotate, for example, add this line to the animation closure.

view.transform = CGAffineTransform(rotationAngle: .pi/4)

If we want to apply more than one transformation, we should concatenate them. For example:

view.transform = 
CGAffineTransform(rotationAngle: .pi/4)
.concatenating(CGAffineTransform(scaleX: 0.5, y: 0.5))

Note that in this example we are updating the view size with both the frame and scale properties. It can be tricky! We can update the size using just one of them.

Feel free to try some other properties, before we continue. That's fun mixing a lot of them, updating some appearance properties and checking the results!

What about Auto Layout? 🤔

Auto Layout animations works a bit different. Let's setup some code to see how it happens.

This is the setup we need:

import UIKit
import PlaygroundSupport
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))PlaygroundPage.current.liveView = containerViewlet view = UIView()
containerView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
view.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
view.widthAnchor.constraint(equalToConstant: 200).isActive = true
view.heightAnchor.constraint(equalToConstant: 200).isActive = true
view.layoutIfNeeded()
view.backgroundColor = .green

To animate views using Auto Layout we should update what we want, but now using the constraints.

For example, to animate the view moving it and increase its size, we should update the constraints related to size and position. To do that, we need to have a reference of the constraints and update them in the animations closure.

Update the code related to the leading and the height constraint:

let leadingConstraint = view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor)
leadingConstraint.isActive = true
...let heightConstraint = view.widthAnchor.constraint(equalToConstant: 200)
heightConstraint.isActive = true

Now we can update these constraints in the animation closure.

UIView.animate(withDuration: 3) {
leadingConstraint.constant = 50
heightConstraint.constant = 250
containerView.layoutIfNeeded()
}

It works! 😉

Maybe you're wondering why we're using layoutIfNeeded(). The answer is because when we call layoutIfNeeded() we're forcing the view to update its layout immediately.

We need to call it before animate, after setting the constraints, otherwise that constraints settings will be animated as well. And we need to call it after all the updates in the animations closure for it takes effect.

Note that Auto Layout works with position and size. The alpha and backgroundColor still works the way they were before.

And what about transformations?

Yes, this can be a bit different than you were expecting, but it still works, even with Auto Layout. According to Apple's Documentation:

In iOS 8.0 and later, the transform property does not affect Auto Layout. Auto layout calculates a view’s alignment rectangle based on its untransformed frame.

Try it out for yourself!

Spring with damping

We can call animate to perform like a spring with damping. We just need to pass three more parameters on the animate function. The parameters are:

delay: The delay until the animation starts.

usingSpringWithDamping: It will be the damping of the spring. The values is from 0 to 1, depending how strength of the damping we want. The higher, the stronger.

initialSpringVelocity: The initial "kick" of the animation. The higher, the stronger.

To learn better how it works, it's good to try with different values. The best ones I used was something between 0.4 and 0.6 to usingSpringWithDamping and 10 to initialSpringVelocity.

Animate key frames

We can perform animations in sequence. In order to do that, we have the animateKeyFrames function. In this case, we need to set a duration for all the animations, and add each animation with a relative start and duration.

For example, if we want to move the view in 0.2 seconds, change its size in 0.2 seconds, rotate in 0.3 seconds and go back to the initial state in 0.3 second, we should do something like the code below.

UIView.animateKeyframes(withDuration: 1, delay: 0, options: [], animations: {    UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.2, animations: {
leadingConstraint.constant = 100
view.layoutIfNeeded()
})
UIView.addKeyframe(withRelativeStartTime: 0.2, relativeDuration: 0.2, animations: {
heightConstraint.constant = 100
view.layoutIfNeeded()
})
UIView.addKeyframe(withRelativeStartTime: 0.4, relativeDuration: 0.3, animations: {
view.transform = .init(rotationAngle: .pi/4)
view.layoutIfNeeded()
})
UIView.addKeyframe(withRelativeStartTime: 0.7, relativeDuration: 0.3, animations: {
leadingConstraint.constant = 0
heightConstraint.constant = 200
view.transform = .identity
view.layoutIfNeeded()
})
})

Note everything will happens in one second. If we change to two seconds, the first animation will happen in 0.4 seconds, because it's proportionally to the total duration. The same will happen to other animation frame durations and start times.

View Controller Transitions

We can animate transitions when view controller calls present or dismiss. To do that, we need to setup a type that implements NSObject and UIViewControllerAnimatedTransitioning. It will be the animator. And the UIViewController that will be animated should implement UIViewControllerTransitioningDelegate.

To animate the present and/or the dismiss we should implement the respective functions:

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?

They will return the animator that we want to perform the present and/or the dismiss.

The UIViewControllerAnimatedTransitioning type (the animator) needs to implement:

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeIntervalfunc animateTransition(using transitionContext: UIViewControllerContextTransitioning)

The first function needs to return the duration of the transition, and in the second it will be where the magic happens.

In the transitionContext we can access the containerView, the view we are transitioning to and the view we are coming from. We can get these views with the functions transitionContext.view(forKey: to) and transitionContext.view(forKey: from).

Basically in the presenting step, we need to add the toView to the containerView (add other views, if we want as well) and animate everything we want. In the dismiss step, we don't need to add the fromView. We just need to animate.

The last step is calling transitionContext.completeTransition(true) in the completion closure, otherwise the transition will never complete.

To check a complete example of that, I suggest check out the branch study02-final. There you will find both animations that I made to study these topics.

The switching lists animation you will find in the function switchNotesOption. Note that I used a snapshotView. This was used to fake that there were two tables in the scene, while there was only one!

The transition animator is the ZoomAnimator and the DiaryViewController implements the UIViewControllerTransitioningDelegate.

This is the end of our study! Drop a message if you have any questions or suggestions! I'd love to hear from you! :)

--

--

Roberto Sampaio
My iOS Studies

Servo de Jesus, aprendendo a escrever, desenvolver software, lutar e tocar guitarra. Aprendendo, sempre.