Chapters

Hide chapters

iOS Animations by Tutorials

Seventh Edition · iOS 15 · Swift 5.5 · Xcode 13

Section IV: Layer Animations

Section 4: 9 chapters
Show chapters Hide chapters

6. View Animations in Practice
Written by Marin Todorov

Looking at the reference of available animation APIs, it’s easy to fall into the trap of thinking view animations are limiting to fading in or out and moving views around the screen. In this chapter, you will work through three practical examples of combining different animations in order to create more complex, non-trivial visual effects.

Having said that, please note that this chapter is optional. If you would like to keep on learning about new APIs, feel free to skip to the next chapter — no hard feelings!

In this chapter you are going to add some cool animations to dress up a flight summary screen, like the one shown below:

There are a few new effects to be found in this chapter that build on the animation foundations you learned in the previous chapters:

  1. Crossfade animation: An animation that blends one image into another.

  2. Cube transition animation: A transition animation that creates a faux 3D transition effect.

  3. Fade and bounce transition: A slightly different take on the combination of simple animations, auxiliary views and everything else you learned up to this point.

This chapter is juicy with code, so head on in to the next section to get started right away!

Crossfading Animations

Open the starter project from the Resources folder for this chapter. Build and run your project to get acquainted with what you have to work with. The app currently shows the flight summary for a trip from London to Rome with a layover in Paris.

The flight summary screen alternates between the two connecting flights and shows items such as the destination, flight number and gate number:

Your first task is to smoothly transition, or blend, between the two background images.

Your first instinct might be to simply fade out the present image and then fade in the new one. But this approach would show the content behind the image as the alpha approached zero; you want the content behind this screen to remain hidden during the entire animation.

Fading the images out, then fading them in again would result in an odd-looking transition like the following:

Obviously, you’ll need a different approach to solve this problem.

Open ViewController.swift and add the following new method to handle your new crossfade effect:

func fade(imageView: UIImageView, toImage: UIImage, showEffects: Bool) {
  UIView.transition(with: imageView, duration: 1.0, 
    options: .transitionCrossDissolve, 
    animations: {
      imageView.image = toImage
    }, 
    completion: nil
  )

  UIView.animate(withDuration: 1.0, delay: 0.0,
    options: .curveEaseOut, 
    animations: {
      self.snowView.alpha = showEffects ? 1.0 : 0.0
    }, 
    completion: nil
  )
}

The method takes the following three parameters:

  1. imageView: This is the image view you are going to fade out.

  2. toImage: This is the new image you want visible at the end of the animation.

  3. showEffects: This is a boolean flag indicating whether the scene should show or hide the snowfall effect. The snowfall animation should only run on the first connecting flight’s screen, where presumably the airport is experiencing bad weather.

The transition animation you use this time is of type .transitionCrossDissolve — this allows you to simply change the image view’s image to the one supplied as a parameter and UIKit automatically creates a cross-fade transition.

Additionally, you fade snowView in or out in parallel with the rest of the animation depending on the value of showEffects.

So where do you trigger fade(imageView:,toImage:,showEffects:) from? Currently changeFlight(to:) switches all the data about the two connecting flights.

Since you are going to eventually animate all of the UI elements on this screen, update the signature of that method as follows:

func changeFlight(to data: FlightData, animated: Bool = false) {

Above, you supply a default value of false for the new animated parameter so that existing calls to this method work as they did before, with no animation.

Next, find the code inside changeFlight(to:,animated:) that updates both the image view and the snow view:

bgImageView.image = UIImage(named: data.weatherImageName)
snowView.isHidden = !data.showWeatherEffects

Wrap it in an if statement that optionally animates the transition as shown below:

if animated {
  fade(imageView: bgImageView, 
    toImage: UIImage(named: data.weatherImageName)!, 
    showEffects: data.showWeatherEffects)
} else {
  bgImageView.image = UIImage(named: data.weatherImageName)
  snowView.isHidden = !data.showWeatherEffects
}

This method calls fade(imageView:, toImage:, showEffects:) when animated is true and passes in the proper parameters from data.

If you want to see what’s inside of data, have a look inside FlightData.swift. The FlightData structure is the model that represents a single flight; there’s two of those flights already pre-defined in FlightData.swift: londonToParis and parisToRome.

Back in ViewController.swift you still need to make one more change before you can see the crossfade effect on screen You need to add the animated parameter to the line where you call changeFlight(to:, animated:).

In changeFlight(to:animated:) change the call to self.changeFlight near the end of the method inside the delay call as follows:

self.changeFlight(to: data.isTakingOff ?   
  parisToRome : londonToParis, animated: true)

Build and run your app; you should now see the image views transition smoothly:

The images blend beautifully into each other, and since you fade the snow effect at the same time, the animation looks seamless. You can even see it snow in Rome for a split second!

You’ve learned an important technique here: transitions can be used to animate changes to non-animatable properties of your views.

Note: If you want to have some fun, you can try also some of the transition effects from the previous chapter. Keep in mind that transitions like .transitionFlipFromLeft are simply too distracting for the current project. .transitionCrossDissolve is a subtle “background” effect which only enhances the animations, which will happen in the foreground.

That takes care of the image view, but the text labels for the flight number and gate number look like they could use some creative animation:

You’ll take care of those next with a faux-3D transition.

Cube Transitions

The effect you are going to build in this section makes it look like the flight and gate information is on adjacent sides of a cube that rotates around its center to reveal the next value. When complete, your animation will work as illustrated below:

This is not a real 3D effect, but it looks pretty close and it’s a great opportunity for you to try animations featuring auxiliary views.

Your approach will be to add a temporary label, animate the heights of the two labels simultaneously, remove the temporary label and, in the end, clean up after yourself.

This will be also the first animation you create that has a direction, since you’ll play it both forwards and backwards. The direction will determine whether the temporary label appears from the top or bottom.

Start by adding the following enumeration inside the ViewController class:

enum AnimationDirection: Int {
  case positive = 1
  case negative = -1
}

Your animation method will take a parameter that sets the animation direction.

Next, add the initial version of that method as shown below:

func cubeTransition(label: UILabel, text: String, direction: AnimationDirection) {
  let auxLabel = UILabel(frame: label.frame)
  auxLabel.text = text
  auxLabel.font = label.font
  auxLabel.textAlignment = label.textAlignment
  auxLabel.textColor = label.textColor
  auxLabel.backgroundColor = label.backgroundColor
}

The method takes three parameters:

  1. label: The label that you want to animate.
  2. text: The new text to display on the label.
  3. direction: The location from where you animate the new text label; this is either the top or the bottom of the view.

To start, you’ll create a new label auxLabel and copy all key properties of the existing label to it, including frame, font, and alignment.

The single difference between the two labels is the text they each contain: the auxiliary label will contain the new text.

Having the two labels appear at the same position is not what you want. Instead, you need to move the auxiliary label away from the existing view and make it really tiny.

Keep going with the transition! Add the following code to end of cubeTransition:

let auxLabelOffset = CGFloat(direction.rawValue) *
  label.frame.size.height/2.0

auxLabel.transform = 
  CGAffineTransform(translationX: 0.0, y: auxLabelOffset)
  .scaledBy(x: 1.0, y: 0.1)
)

label.superview?.addSubview(auxLabel)

In the code above, you first calculate the vertical offset for the auxiliary label. The raw value of direction is either 1 or -1, which gives you the proper vertical offset to position the temporary label.

Next, you adjust the transform of the auxiliary label to create a faux-perspective effect. When you scale the text on its Y-axis alone, it’ll look squashed like you’re viewing the plane of the text on edge:

Finally, you add the newly created label at the same level in the hierarchy as the existing label.

Next add the following animation code to the end of cubeTransition:

UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut, 
  animations: {
    auxLabel.transform = .identity
    label.transform =
      CGAffineTransform(translationX: 0.0, y: -auxLabelOffset)
      .scaledBy(x: 1.0, y: 0.1)
    )
  }, 
  completion: { _ in
    label.text = auxLabel.text
    label.transform = .identity

    auxLabel.removeFromSuperview()
  }
)

In the animations block you reset the transform of auxLabel; this makes the new text grow in height and positions it exactly on top of the old one.

Speaking of the old text, you also apply a transform to label that scales it down and moves it in the direction opposite to where the new text appears.

Insert the following code in changeFlight(to:animated:) inside the if statement, just below where you call fadeImageView:

let direction: AnimationDirection = data.isTakingOff ?   
  .positive : .negative

cubeTransition(label: flightNr, text: data.flightNr, direction: direction)
cubeTransition(label: gateNr, text: data.gateNr, direction: direction)

In the same method, move the existing code that sets the flightStatus, flightNr, gateNr, departingFrom, arrivingTo text inside the else statement, otherwise those statements will change the label’s text immediately and spoil your animation.

The completed else block should look now like this:

} else {
  bgImageView.image = UIImage(named: data.weatherImageName)
  snowView.isHidden = !data.showWeatherEffects
  
  flightNr.text = data.flightNr
  gateNr.text = data.gateNr

  departingFrom.text = data.departingFrom
  arrivingTo.text = data.arrivingTo
  
  flightStatus.text = data.flightStatus
}

Build and run your project; enjoy the results of your labor as you watch the gate number and flight number animate via your fancy faux-3D transition.

Notice how running the animation both forwards and backwards gives a real impression that you’re switching between two alternate states. This screen is becoming more and more alive, thanks to you!

You’re not quite done yet; you’ll finish off this chapter by adding a composite animation to the three-letter airport codes on the screen to make the screen transitions feel organic and snappy!

Fade and Bounce Transitions

The final animation in this chapter makes use of labels, auxiliary views, and everything else you’ve learned up to this point. Consider it a chapter review, if you will!

Start by adding the following new method for the new transition animation:

func moveLabel(label: UILabel, text: String, offset: CGPoint) {
  let auxLabel = UILabel(frame: label.frame)
  auxLabel.text = text
  auxLabel.font = label.font
  auxLabel.textAlignment = label.textAlignment
  auxLabel.textColor = label.textColor
  auxLabel.backgroundColor = .clear
  
  auxLabel.transform = CGAffineTransform(translationX: offset.x, y: offset.y)
  auxLabel.alpha = 0
  view.addSubview(auxLabel)
}

The new method takes three parameters:

  1. label: The label you want to animate.

  2. text: The new text you want to display.

  3. offset: The arbitrary offset you’ll use to animate the auxiliary label.

The code above is pretty simple: just as you did in the previous section, you create an auxiliary label and copy all properties to it from the existing one. To create the label transform, you simply use the offset parameter.

Finally, just before you add the new label to the view controller’s view, you hide it by setting its alpha property to 0.0.

That sets the stage for your animations. Next you’re going to exchange the places of the original and auxiliary labels. This time, instead of using the same animation for both labels, you’ll create separate animations for each and move them independent of each other. This will result in an interesting and organic visual effect.

First add the following code to the end of moveLabel:

UIView.animate(withDuration: 0.5, delay: 0.0, 
  options: .curveEaseIn, 
  animations: {
    label.transform = CGAffineTransform(translationX: offset.x, y: offset.y)
   label.alpha = 0.0
  }, 
  completion: nil
)

This animation moves the label away from its original position and fades it out.

Next, add the following code to animate the auxiliary label:

UIView.animate(withDuration: 0.25, delay: 0.1, options: .curveEaseIn, 
  animations: {
    auxLabel.transform = .identity
    auxLabel.alpha = 1.0
  }, 
  completion: { _ in
    //clean up
  }
)

You reset the auxiliary label’s transform above, effectively moving it to its original location. You also fade the text in by animating its alpha value.

Note that this animation kicks off after a 0.1 second delay and it lasts only 0.25 seconds. This means that the two labels will overlap while exchanging places, creating a subtle yet pleasing “ghost” effect.

Before you can test your new animation, you need to add the code to remove the auxiliary label when the animation is complete.

Find the comment //clean up in the completion block of the code you just added and replace it with the following code:

auxLabel.removeFromSuperview()
label.text = text
label.alpha = 1.0
label.transform = .identity

This wraps up the transition nicely, returns the label to its initial state and sets the new text of the label.

Now find changeFlight(to:,animated:) and add the following code to the bottom of your if animated { statement:

let offsetDeparting = CGPoint(
  x: CGFloat(direction.rawValue * 80),
  y: 0.0)

moveLabel(label: departingFrom, text: data.departingFrom,
  offset: offsetDeparting)

let offsetArriving = CGPoint(
  x: 0.0,
  y: CGFloat(direction.rawValue * 50))

moveLabel(label: arrivingTo, text: data.arrivingTo,
  offset: offsetArriving)

This new method takes an arbitrary offset as a parameter. This lets you create two different animations for the departure and arrival airports.

For the departure label, you create a horizontal movement; for the arrival airport, you create a vertical movement instead.

That’s all the code you need! Build and run your app to see all labels animate via your snappy new transition effect.

Take a moment to appreciate that even in this impressive-looking animation, you’ve still only animated a few properties on any given view. You’ve worked with bounds, frame, center, transform, backgroundColor and alpha — but you’ve managed to create some very impressive animations with those properties alone!

Animating each property on its own might not always result in mind-blowing effects, but using a combination of a number of simple animations and using subtle yet powerful techniques such as adding auxiliary views and transitions can produce impressive visual effects.

Let your imagination… take flight!

Key Points

  • You are not limited to animating a single view property from a single animation call; you can combine and overlap animations freely.
  • To create complex effects you might use any and all “tricks” you feel the task at hand calls for, including creating temporary views for the duration of the animation.

Challenges

Challenge 1: Animate the Flight Status Banner

So far there’s still one UI element that doesn’t animate when the screen switches between the two connecting flights — the flight status:

The piece of text on the banner saying “Boarding” is a UILabel; as such, you can use either of the two label animations you created in this chapter to show the alternate flight statuses.

I believe you can figure out how to animate the banner on your own; use the flightStatus outlet to access the label and data.flightStatus to get the new text to animate to.

Feel free to experiment and play around further. The more experience you gain, the better understanding you will have of animations in UIKit!

Now that you’re a pro at animations and transitions, it’s time for the final topic on view animations — combining multiple animation steps together with keyframes.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.