26.
Simple 3D Animations
Written by Marin Todorov
In this chapter you’ll get to try out your newfound knowledge about camera distance and perspective.
Once you set up your layer’s perspective, you can work on the layer’s transform as you usually would; but now you can rotate, translate and scale your layer in three dimensions.
The project for this chapter features a folding pull-out menu as popularized in many apps, such as Taasky:
Office Buddy is an office helper app for employees to access categorized information about day-to-day company life.
The starter project already has all the code to make the menu functional, but it only works in 2D. Your task is to bring the menu into the third dimension and give it life!
Creating 3D transformations
Open the starter project for this chapter and build and run it to see what the initial version of the Office Buddy app looks like:
Tap the menu button to reveal the side menu; alternatively, you can swipe right to reveal the side menu.
As you can see, this app is as flat as they come. But armed with your new insight on 3D perspective, you’re going to add some depth to the menu.
Open ContainerViewController.swift; this controller displays both the menu view controller and the content view controller on the screen. It also handles pan gestures so the user can open and close the menu.
Your first task is to build a class method that creates the corresponding 3D transform for a given percentage of “openness” of the side menu.
Add the following method declaration to ContainerViewController.swift:
func menuTransform(percent: CGFloat) -> CATransform3D {
}
The above method accepts a single parameter of the current progress of the menu, which was calculated by the code in handleGesture(_:), and returns an instance of CATransform3D. You’re going to assign the result of this method directly to the menu layer’s transform property.
Add the following code to your new method:
var identity = CATransform3DIdentity
identity.m34 = -1.0/1000
This code might look a bit surprising; so far you’ve only used functions to create or modify transforms. This time, however, you’re modifying one of the class’ properties.
Note: Why is this property called
m34?View and layer transforms are expressed as two-dimensional math matrices. In the case of a layer transform matrix, the element in the3rdrow at the4thcolumn sets your z-axis perspective. You can set this element directly to apply the desired perspective transform.
So you create a new CATransform3D struct and you set its m34 property to -1.0/1000… why would you choose that value?
Okay, I’ll back up a little. To enable 3D transforms on a layer you need to set m34 to -1.0 / [camera distance]. Since you read through the introduction for this section (you did read it, right?), you have some understanding of how the camera distance affects the scene perspective.
But why are you using 1000 for the camera distance? The distance is expressed in points between the camera and the front of the scene. As to what value to use, the truth is that you need to try different values and see what looks good for your particular animation.
Working with camera distance
For UI elements in an average app you can consult the following reference for some guidelines on an appropriate camera distance:
-
0.1...500: Very close, lots of perspective distortion. -
750...2,000: Nice perspective, content is clearly visible. -
2,000and up: Almost no perspective distortion.
For the Office Buddy app, a distance of 1000 points will give the menu a nice subtle perspective. You can see it in action once you finish working on the current method.
Add the following code to the bottom of menuTransform(percent:):
let remainingPercent = 1.0 - percent
let angle = remainingPercent * .pi * -0.5
In the above code you calculate the current angle of the menu based on its “openness” value.
Now add the following code to the bottom of menuTransform(percent:):
let rotationTransform = CATransform3DRotate(
identity, angle, 0.0, 1.0, 0.0)
let translationTransform = CATransform3DMakeTranslation(
menuWidth * percent, 0, 0)
return CATransform3DConcat(
rotationTransform, translationTransform)
Here, you use rotationTransform to rotate the layer away from you around its y-axis.
The menu is moving in from the left, so you also create a translation transform to move it along the x-axis, eventually to menuWidth at 100%.
Finally, you concatenate the two transforms and return the result.
Now you can use menuTransform(percent:) to update the menu transform as the user pans right or left.
Remove the following line from setMenu(toPercent:) that modifies the menu’s origin:
menuViewController.view.frame.origin.x =
menuWidth * CGFloat(percent) - menuWidth
You don’t need the above line anymore because you will move the menu via its transform. Add the following code to setMenu(toPercent:):
menuViewController.view.layer.transform =
menuTransform(percent: percent)
Build and run your project; pan right to see how the menu rotates around its y-axis:
The menu rotates in 3D, but it’s rotating around its horizontal center, which separates the menu from the content view controller.
Moving the layer’s anchor point
By default, the anchor point of a layer has an x coordinate of 0.5, meaning it is in the center. You need to set the x of the anchor point to 1.0 to make the menu rotate around its right edge like a hinge, as shown below:
All transforms are calculated around the layer’s anchor point. You also use the anchor point as your reference point when setting the position of the layer. This means it’s best to change the layer’s anchorPoint before you set its position on the screen.
Find the following line in viewDidLoad() where you set the menu frame:
menuViewController.view.frame = CGRect(
x: -menuWidth, y: 0,
width: menuWidth, height: view.frame.height)
Now insert the following code just above that line (it’s important to insert the line before you set the view frame because otherwise setting the anchor point will offset the view — if you’re curious to see the difference give both ways a try):
menuViewController.view.layer.anchorPoint.x = 1.0
This rotates the menu around its right edge.
Build and run your project again, then pan horizontally through the view and observe how the effect has changed:
That looks much better!
You’re almost done — there’s just a few more bits to take care of.
Creating perspective through shading
Shading lends a lot of realism to 3D animations; to that end, you will rotate the menu out of the “shadow” of the left side of the content view controller.
You’re not using any advanced shading techniques here; instead, you can simulate this by changing the alpha of the menu as it rotates.
Add the following code to setMenu(toPercent:):
menuViewController.view.alpha = CGFloat(max(0.2, percent))
In the code above, you assign the percent value directly to the layer’s alpha, but you limit it to 0.2 to ensure the menu remains visible when it’s edge-on to the user and doesn’t disappear completely.
Since the background of this app is black, lowering the alpha of the menu view makes the black color show through the menu and simulates a shadow effect.
Build and run your project and observe the effect:
It’s a small detail, but it really makes your 3D animation “pop”!
You might have noticed that when you tap the menu button the first time, the animation does not play in 3D. The effect only kicks in on the second and subsequent animations.
That’s because you’re not setting the 3D animation parameters and the layer transforms until after the first time you toggle the menu. This is easy to fix: As soon as the menu view controller loads, set the menu progress to 0.0 to set the proper menu layer transform.
Add the following to the bottom of viewDidLoad():
setMenu(toPercent: 0.0)
This will get the starting frames and layer transforms in place from the start. Build and run your project again; tap the menu button and you’ll see that the animation works properly the first time through.
Rasterizing for efficiency
There’s one last task to make your animation “perfect”. If you stare at the menu long enough while you pan back and forth you’ll notice the borders of the menu items look pixelated.
Core Animation continually redraws all contents of the menu view controller and recalculates the perspective distortion for all elements as it moves, which isn’t terribly efficient — hence the jagged edges.
It’s better to let Core Animation know that you won’t change the menu contents during the animation so that it can render the menu once and simply rotate the rendered and cached image.
That sounds complicated at first, but you’ll see it’s quite easy to implement. Scroll to handleGesture() and find the .began case block; this code executes when the user starts the pan action. This is where you’ll instruct Core Animation to cache the menu.
Add the following code to the end of the .began case block:
// Improve the look of the opening menu
menuViewController.view.layer.shouldRasterize = true
menuViewController.view.layer.rasterizationScale =
UIScreen.main.scale
shouldRasterize instructs Core Animation to cache the layer contents as an image. You then set rasterizationScale to match the current screen scale and you’re golden!
Build and run your project again to see how your graphics have improved:
Core Animation really shines in this instance. Working with 2D images is one of the things this framework really does well!
To avoid any unnecessary caching while using the app, you should turn off rasterization as soon as the animation is done.
Find the empty animation completion closure inside the .failed case and add the following code:
self.menuViewController.view.layer.shouldRasterize = false
Now you’re only activating rasterization during the animation. How efficient of you!
In only a few pages, you learned about camera distance, perspective, and how to set up your 3D scene and apply animations to it.
There’s one more chapter in this section, which contains some more 3D fun — but before you go, take a look at this chapter’s challenge first.
Key points
- The
.m34value of aCATransform3Dis important as it gives perspective to your 3D transforms. - The anchor point of a layer sets the point around which your transforms take place.
Challenges
Challenge 1: Create your own 3D animation
For this challenge you are going to create a 3D rotation animation for the menu button. As the user pans the button will rotate alongside the menu view controller.
Specifically, you will create a rotation around both the x- and y-axes to make the menu button flip on its diagonal.
Add the following code to setMenu(toPercent:) in ContainerViewController.swift:
let centerVC = centerViewController.viewControllers.first as? CenterViewController
This fetches the current content view controller so you can work with it.
The menu button is accessible via the menuButton property of CenterViewController. For this challenge, adjust the 3D transform of the button’s imageView.
If you directly rotate the button rather than use the 3D transform, it will clash with the navigation bar views underneath and might get partially obscured by the navigation bar.
In contrast, the button’s own 2D space is already positioned on top of all navigation bar views before you rotate it — so if you rotate menuButton.imageView, it rotates within the button’s own plane, which is on top of the navigation bar at all times.
Another gotcha is that the first time the code runs menuButton will be nil, so you should treat it like an optional, even though it is implicitly unwrapped.
For the rotation, create a transform just like you did earlier in this chapter, but this time rotate the view around the x- and y-axes. If necessary, have a look at the documentation for CATransform3DRotate().
This time you don’t need to calculate the remaining percentage; to get the rotation angle, simply multiply the progress by .pi.
When you are finished with your solution the button image should flip around while you pan through the screen like so:
That’s it! Head on in to the next chapter for more awesome animations in 3D!