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

27. Intermediate 3D Animations
Written by Marin Todorov

In the previous chapter, you learned that applying perspective to a single view isn’t a complicated task; in fact, once you know the secret of m34 and camera distance you can create all kinds of 3D animations.

This chapter builds on what you’ve already learned and shows you how to create convincing 3D animations with more than one view.

The starter project for this chapter is a simple hurricane image gallery. By the end of this chapter, you’ll have a 3D effect to get an overall view of the images in the gallery:

You’ll be able to tap on a photo to bring it full screen, and tapping the top right button fans all the images open again — with a cool animation to take you between the two states, of course!

Ready to get started? Hang on to your hat and prepare to get blown away by this “stormy” project!

Exploring the Starter Project

Open the starter project from the Resources folder for this chapter; build and run it to see what you have to start with:

All you have is a blank screen with two bar buttons on top: the left one shows the NASA image credits and the right one invokes the method that shows or hides the gallery as appropriate.

First, you’ll need to display all images on the screen and set them up so they’re ready for your “fan” animation.

Open ViewController.swift and inspect the class code. You’ll see an array called images; this array contains some slightly customized image views. The ImageViewCard class inherits from UIImageView and adds a string property title to hold the hurricane title, and a property called didSelect so you can easily set a tap handler on the image.

Your first task is to add all images to the view controller’s view. Add the following code to the end of viewDidAppear(_:):

for image in images {
  image.layer.anchorPoint.y = 0.0
  image.frame = view.bounds
  
  view.addSubview(image)
}

In the code above, you loop over all images, set each image’s anchor point to 0.0 on the y-axis and resize each image so it takes up the full screen. When that’s done, you add each image to view.

Setting the anchor point lets the images rotate around their upper edge rather than the default of the center, as illustrated below:

This will make it a lot easer to fan out the images in 3D space.

Build and run your project to see what you’ve achieved so far:

Hmm — you only see the last image you added to view. That’s because all images have the same frame, so you only see the last image you added: Hurricane Irene.

To make it more obvious which hurricane image is being displayed, add the following line at the end of viewDidAppear(_:):

navigationItem.title = images.last?.title

This code takes the name of the last hurricane image in the collection and sets it as the navigation title of the view controller, like so.

Build and run again to familiarize yourself with Irene:

Notice that you didn’t set any perspective transforms on the images; you’re going to set a perspective directly on the view controller’s view instead.

In the previous chapter you adjusted the transform property on a single view and then rotated it in 3D space. But since your current project has more individual views that you’d care to manipulate in 3D, you can set the perspective of their parent view instead to save yourself a bunch of work.

Add the following code to viewDidAppear(_:):

var perspective = CATransform3DIdentity
perspective.m34 = -1.0 / 250.0
view.layer.sublayerTransform = perspective

Here you use the layer property sublayerTransform to set the perspective of all sublayers of the view controller’s layer. The sublayer transform is then combined with each individual layer’s own transform.

This lets you focus on managing the rotation or translation of your subviews without having to worry about perspective. You’ll see how this works in more detail in the next section.

Transforming the Gallery

toggleGallery(_:) is hooked up to the Browse bar button on the right and is where you’ll apply your 3D transform to the four images.

Add the following variable to toggleGallery(_:):

var imageYOffset: CGFloat = 50.0

Since you don’t just rotate all images in place but simply move them around to produce the “fan” animation, you use imageYOffset to set the offset of each image.

Next you need to iterate through all the images and run their individual animations.

Add the following code to toggleGallery(_:):

for subview in view.subviews {
  guard let image = subview as? ImageViewCard else {
    continue
  }

  // more code here
}

Here, you loop through all subviews of the view controller’s view and act only on the subviews that are instances of ImageViewCard.

Add the following code after the guard block you added above, to replace the more code here comment:

var imageTransform = CATransform3DIdentity

// 1
imageTransform = CATransform3DTranslate(  
  imageTransform, 0.0, imageYOffset, 0.0)

// 2
imageTransform = CATransform3DScale(  
  imageTransform, 0.95, 0.6, 1.0)

// 3
imageTransform = CATransform3DRotate(  
  imageTransform, .pi / 8, -1.0, 0.0, 0.0)

You start by assigning the identity transform to imageTransform and then add a series of adjustments to it. This is what each individual adjustment does to the image:

  1. Move the image on the y-axis with CATransform3DTranslate; this offsets the image from its default 0.0 y-coordinate as shown below:

Later, you’ll calculate the imageYOffset of each image separately; for now all images move by the same amount so you’ll still see only the top one for the moment.

  1. Scale the image by adjusting the scale component of the transform using CATransform3DScale. You shrink the image just a little on the x-axis, but you scale it down to 60% on the y-axis to enrich the rotation 3D effect:

  1. Finally, you use CATransform3DRotate to rotate the image by 22.5 degrees to give it some perspective distortion as shown below:

Remember, you already set the anchor point so the image rotates around its top edge.

Now you see the value of setting the m34 value above via view.layer.sublayerTransform; your rotation transform simply re-uses the m34 value from the sublayer transform, without the need to apply it here. That’s handy!

Now all that’s left is to apply the transform to each image. Add the following line (still inside the for body):

image.layer.transform = imageTransform

Build and run your project; tap the Browse button to see the result of your transforms:

Again, you only see the top image as all other images behind it have the same transform applied. Now is a perfect opportunity to customize the transform for each image.

Add the following line to the end of the for block:

imageYOffset += view.frame.height / CGFloat(images.count)

This adjusts the y-offset of each image depending on where it is in the stack. You divide the screen height over the number of images so they distribute themselves evenly over the screen.

Build and run your project to see your views fan out:

Sweet — the gallery looks just as you intended! But as this is a book about animations, it would be a shame if you didn’t animate the transition to the fanned-out view, wouldn’t it?

Animating the Gallery

Find the following line in toggleGallery(_:) where you set transform on each image:

image.layer.transform = imageTransform

Insert the following code above that line to animate transform:

let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = NSValue(caTransform3D:
  image.layer.transform)
animation.toValue = NSValue(caTransform3D: imageTransform)
animation.duration = 0.33
image.layer.add(animation, forKey: nil)

This code is definitely familiar: You create a layer animation on the transform property and animate it from its current value to the imageTransform you designed earlier.

Build and run your project once more; tap the Browse button and enjoy your completed animation:

You’re finished with the gallery for now; you’ll revisit it in the Challenges section when you add the ability to close the fan when the user taps the Browse button.

Bringing an Image to the Front

In this final section, you’ll add a bit of interactivity to the image gallery: tapping an image will make it jump in front of the other images so that the user can get a better look at it ImageViewCard already features a closure expression property named didSelect; this fires when the user taps on the image and receives the tapped image view as an input parameter.

To add this feature, you’ll add a method to ViewController and assign it to didSelect on all the image views.

First, add the following code to viewDidAppear(), inside the for loop body:

image.didSelect = selectImage

Xcode will complain that selectImage doesn’t exist, but you’ll fix this in the next step.

Add the following method to ViewController:

func selectImage(selectedImage: ImageViewCard) {
  for subview in view.subviews {
    guard let image = subview as? ImageViewCard else {
      continue
    }
    if image === selectedImage {
      //selected image
    } else {
      //any other image
    }
  }
}

This is the skeleton of selectImage(selectedImage:); when the user taps one of the images, you loop over all subviews and get the ImageViewCard instances just like you did earlier. Then you check each ImageViewCard to see if it’s the selected image.

Now you need two more animations: one to animate the selected image, and another to animate all the other images in the gallery.

You’ll tackle this in reverse and fade out the unselected images first.

Replace the //any other image comment in selectImage(selectedImage:) with the following code:

UIView.animate(
  withDuration: 0.33, 
  delay: 0.0, 
  options: .curveEaseIn, 
  animations: {
    image.alpha = 0.0
  }, 
  completion: { _ in
    image.alpha = 1.0
    image.layer.transform = CATransform3DIdentity
  })

This is a simple animation to fade out each image. In the completion block, you reset its transform to the identity transform and alpha to fully opaque. By the time the above animation is done, the selected image will be in front of all others — so transparent or not, you won’t see any of the unselected images. Resetting alpha here saves you from having to remember to reset it when you want to see the image again.

Build and run your project; open the gallery and tap on an image to see all the other unselected images fade from view.

When that animation completes, the view changes back to show the top image at full-screen — because you haven’t done anything with the selected image! You’ll fix that now.

You’ll animate the selected image’s transform back to the identity transform and pull the image to the front when the animation completes.

Replace the //selected image comment in selectImage(selectedImage:) with the following code:

UIView.animate(
  withDuration: 0.33, 
  delay: 0.0, 
  options: .curveEaseIn, 
  animations: {
    image.layer.transform = CATransform3DIdentity
  }, 
  completion: { _ in
    self.view.bringSubviewToFront(image)
  })

Here, you’re un-doing the 3D transform for the animation, and then ensuring the image is at the top of the view stack at the end so it’s visible.

Finally, add the following code to the end of selectImage(selectedImage:):

self.navigationItem.title = selectedImage.title

This will update the navigation bar with the currently selected image title. Build and run your project; tap the various images to see how they zoom to full screen. How does it look? I thought you’d enjoy this classy little animation! That’s a wrap; there’s one small bit of functionality to clean up in the Challenge section below, but other than that, you have a really stunning animation at your fingertips. I suspect you can think of many ways to use it in your own projects!

Key Points

  • When you set the 3D perspective via the m34 property and use the resulting transform to set sublayerTransform on a layer, all of its sub-layers can be animated in 3D space.
  • You can combine CATransform3D layer animations with UIKit view animations and let Core Animation automatically combine and render them on screen.

Challenges

Challenge 1: Toggle the Gallery with the Browse Button

Right now the user must choose an image from the gallery once they open it. In this challenge, you’ll make the Browse button work like a toggle to close the gallery view as well. Add a new property to ViewController named isGalleryOpen and set its initial value to false. You need to update the value of this property in couple of places in the code:

  • Set it to true at the end of toggleGallery(_:)

  • Set it to false at the end of selectImage(selectedImage:)

In the for loop in toggleGallery(), add a check to see if the gallery is already open. If so, leave imageTransform as it is, instead of adding the translate, scale and rotation transforms to it. Don’t forget to toggle() isGalleryOpen at the end of the method.

That’s it — if you like, play around with the animations in this project and see what other flashy elements you can add on your own!

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.
© 2026 Kodeco Inc.