This is one of my notes on learning iOS Animations by Tutorials. The code in detail on andyRon/LearniOSAnimations my lot.

So far, previous articles have only used two-dimensional animation — the most natural way to animate elements on a flat device screen. After all, buttons, text fields, switches, and images from the flattened world of iOS 7 have no third dimension; These elements exist in the plane defined by the X and Y axes:

Core animation helps us get out of this two-dimensional world; While it’s not really a 3D framework, core animation has a lot of good ways to draw two-dimensional objects in 3D space.

In other words, layers and animations are still drawn in two dimensions, but the 2D plane of each element can be rotated and positioned in 3D space, as shown below:

Shown above are two 2D images rotated in 3D space. Perspective distortion allows us to know their position from the renderer’s point of view.

This article will learn how to position and rotate layers in 3D space. CATransform3D is similar to CGAffineTransform, but in addition to scaling, tilting and shifting in the X and Y directions, it also brings a third dimension: Z. The Z axis is directed from the device screen toward your eyes.

Consider the following examples to better understand how perspective works.

Setting the camera very close to the screen will distort the layer’s perspective accordingly:

If the camera is far away from the object:

Finally, if you set a large distance between the camera and the screen:

Preview:

24- Simple 3D animation – try your newfound knowledge about camera distance and perspective. Set the layer perspective and handle layer transformations to rotate, pan, and scale the 3d layer.

25- Intermediate 3D Animations – Building on the previous chapter, now that you know the secrets of M34 and camera distance, you can create various 3D animations with multiple views.

24- Simple 3D animation

This chapter will examine the newly discovered knowledge about camera distance and perspective.

Office Buddy is an Office help application that gives employees access to classified information about daily corporate life. The app is as simple as clicking the button in the upper left corner or sliding left and right, and then the left sidebar appears. Below 👇 will add some 3D elements to the start project.

Start project preview:

Create 3 dtransformations

Open the ContainerViewController. Swift, ContainerViewController displayed on the screen menu view controller and content view controller. It also handles panning gestures so that the user can open and close menus.

Your first task is to build a class method that creates a 3D transform for a given percentage of “openness” on the side menu. Add the following method declarations to ContainerViewController:

func menuTransform(percent: CGFloat) -> CATransform3D{}Copy the code

The above method takes a single parameter of the current menu progress, calculated by the code in handleGesture(_ :), and returns an instance of CATransform3D. You will assign the result of this method directly to the transform property of the menu layer.

Add the following code to the above method:

var identity = CATransform3DIdentity
identity.m34 = -1.0/1000
Copy the code

This code might seem a little surprising; So far, you have only used functions to create or modify transformations. This time, however, you are modifying the properties of one of the classes.

Note: CATransform3D and CGAffineTransform sub-tables represent 4*4 and 3*3 mathematical matrices, which are both represented by structures in Swift and OC.

Attribute M34 refers to the third row and fourth column of the matrix, which is commonly used to represent perspective effect. M34 = -1 / D, D can be understood as the camera distance. The smaller D is, the more obvious the perspective effect will be.

Camera distance

For a typical UI element in an application, the camera distance might be expressed as: 0.1… 500: Very close, perspective distortion. 750… 2,000: Good viewing Angle and clear content. 2000+ : Almost no perspective distortion.

For Office Buddy applications, a distance of 1000 points provides a very subtle view of the menu.

Add the following code to the bottom of menuTransform(Percent 🙂 :

let remainingPercent = 1.0 - percent
let angle = remainingPercent * .pi * -0.5
Copy the code

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)
Copy the code

In this case, rotate the layer around the Y-axis using rotationTransform. The menu moves from the left, so you also need to create a translation transform to move it along the X-axis, eventually setting the menu width to 100%. Finally, connect the two transformations and return the results.

Remove the following from setMenu(toPercent:) :

menuViewController.view.frame.origin.x = menuWidth * CGFloat(percent) - menuWidth
Copy the code

Alternative is:

menuViewController.view.layer.transform = menuTransform(percent: percent)
Copy the code

The position of the menu bar is controlled by conversion.

Run the item and pan to the right to see how the menu rotates around its Y-axis:

The menu rotates in 3D, but it rotates around its horizontal center, with a gap between the menu and the content view controller.

Move the anchor point of the layer

By default, the layer’s anchor point has an X coordinate of 0.5, which means it is centered. Set the x of the anchor point to 1.0, and the above gap will not appear, as follows:

All transformations are calculated around the anchor points of the layer.

Find the following line in viewDidLoad() :

menuViewController.view.frame = CGRect(x: -menuWidth, y: 0, width: menuWidth, height: view.frame.height)
Copy the code

Now insert the following code above the line (it is important to insert the line before setting the view frame, otherwise setting the anchor point will offset the view) :

menuViewController.view.layer.anchorPoint.x = 1.0
Copy the code

This causes the menu to rotate around its right edge.

Operation effect:

That looks better!

Create vistas with shadows

Shadows bring a lot of realism to 3D animation. There is no need to use any advanced shading techniques, just change alpha when rotating.

Add the following code to setMenu(toPercent:) :

menuViewController.view.alpha = CGFloat(max(0.2, percent))
Copy the code

0.2 makes the menu smallest and still visible, and the percentage makes the menu smaller and less transparent.

Because the background of this application is black, lowering the alpha value of the menu view causes the menu to show black and simulates a shadow effect.

Operation effect:

This is a small detail that makes the 3D more realistic.

If you look closely, you’ll see that the menu isn’t displayed in 3D the first time you click the button, but later. This is because the first time you switch the menu before setting the 3D animation parameters and converting layers. In viewDidLoad() add:

setMenu(toPercent: 0.0)
Copy the code

Rasterization efficiency

Make the animation “perfect”. If you stare at the menu long enough as you pan back and forth, you’ll notice that the border of the menu item looks pixelated, like this:

The core animation constantly redraws everything in the menu view controller and recalculates the perspective distortion of all elements as they move, with jagged edges.

It is best to let Core Animation know that we will not change the menu content during the Animation so that it can render the menu once and simply rotate the rendered and cached images. This sounds complicated, but it’s easy to implement.

Find the.Began code block in handleGesture(), which executes when the user pans.

Add the following code to the end of the.Began block:

menuViewController.view.layer.shouldRasterize = true
menuViewController.view.layer.rasterizationScale = UIScreen.main.scale
Copy the code

ShouldRasterize lets the core animation cache the layer content as an image. Then set the rasterizationScale to match the current screen scale.

Operation, effect:

To avoid any unnecessary caching when using the application, you should turn off rasterization as soon as the animation is complete. Locate the animation completion closure in the. Failed code block and add the following code:

self.menuViewController.view.layer.shouldRasterize = false
Copy the code

Rasterization is now activated only during animation. Improved efficiency! 😊

3D rotation animation of menu buttons

When the menu is displayed, the menu button also rotates itself. Specifically, you create rotations around the X and y axes so that the menu button flips on its diagonal.

In the setMenu(toPercent:) of ContainerViewController add:

let centerVC = centerViewController.viewControllers.first as? CenterViewController
if letmenuButton = centerVC? .menuButton { menuButton.imageView.layer.transform = buttonTransform(percent: percent) }Copy the code

The buttonTransform function is:

func buttonTransform(percent: CGFloat) -> CATransform3D {
    var identity = CATransform3DIdentity
    identity.m34 = -1.0/1000

    let angle = percent * .pi
    let rotationTransform = CATransform3DRotate(identity, angle, 1.0.1.0.0.0)

    return rotationTransform
}
Copy the code

The effect is as follows:

25- Intermediate 3D animation

In the previous chapter 24- Simple 3D Animation, you learned how to animate simple 3D effects by applying perspective to a single view. In fact, once we know the secret of m34 and camera distance, we can create all kinds of 3D animations.

In this chapter, build on the previous content and learn how to create interesting 3D animations using multiple views.

The beginning project of this chapter ***ImageGallery*** is a simple hurricane gallery.

Exploration Initiation Project

The beginning projects of this chapter are:

It’s just a blank screen with two buttons at the top.

Open viewController.swift and you’ll see an array called images, which is just some image information.

The ImagViewCard class inherits from UIImageView and has a string property title to hold the hurricane title, and a property called didSelect so that you can easily set click handlers on the image.

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

for image in images {
    image.layer.anchorPoint.y = 0.0
    image.frame = view.bounds

    view.addSubview(image)
}
Copy the code

In the code above, loop through all the images, set the anchor point for each image to 0.0 on the Y-axis, and resize each image so that it takes up the entire screen. Setting an anchor point rotates the image around its upper edge instead of its center default, as shown below:

Running only sees the last Hurricane Irene image, because the images are in the same position, stacked on top of each other

To display the name of the hurricane image, add the following line to the end of viewDidAppear(_:) :

navigationItem.title = images.last? .titleCopy the code

Note that no perspective transformations are currently set on the image; You will then set the perspective directly on the view controller’s view.

In the previous chapter, you adjusted the Transform property on a single view and then rotated it in 3D space. However, because your current project has more personal views that need to be manipulated in 3D, you can save a lot of work by setting the perspective of its parent.

Add the following code to viewDidAppear(_:) :

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

Here, you can use the layer attribute sublayerTransform to set the perspective for all the sublayers of the view controller layer. The sublayer transformations are then combined with each individual layer’s own transformations.

This allows you to focus on managing the rotation or panning of subviews without worrying about perspective. You’ll see how it works in more detail in the next section.

Change the gallery

ToggleGallery (_:) connects to the browse button in the upper right, where the 3D transformation is applied to the four images.

Add the following variables to toggleGallery(_:) :

var imageYOffset: CGFloat = 50.0

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

Since you don’t just rotate all the images to their original position but just move them to produce a “fan” animation, you can use imageYOffset to set the offset for each image. Next, you need to walk through all the images and run their respective animations.

Here, you loop through all the child views of the view Controller view and perform operations only on the child view that is the ImageViewCard instance. Add the following code after the guard block added above to replace more code comments here:

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)
Copy the code

The identity transform is first assigned to the imageTransform, and then a series of adjustments are added to it. Here’s what each individual adjustment does to the image:

// 1 Use CATransform3DTranslate to move the image on the y axis; This causes the image to deviate from its default 0.0y coordinates, as shown below:

After that, the imageYOffset for each image will be calculated separately, otherwise the images will still be superimposed.

// 2 Scale the image by using CATransform3DScale to adjust the scale component of the transform. You can shrink the image slightly on the X axis, but reduce it to 60% on the Y axis to enrich the rotating 3D effect:

// 3 Finally, use CATransform3DRotate to rotate the image 22.5 degrees so that it has some perspective distortion, as shown below:

Remember that the anchor point was set earlier, so the image rotates around its top edge.

Now you see through the view. The layer. SublayerTransform m34 above the set value of value; Your rotation transform simply reuses the M34 value from the sublayer transform, rather than applying it here. That’s convenient!

All that is left now is to apply the transformation to each image. Add the following line (still in the for block) :

image.layer.transform = imageTransform
Copy the code

Add the following line to the end of the for block to change the position of each image:

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

This adjusts the Y offset for each image, depending on where it is in the stack. Divide the screen height by the number of images so that they are evenly distributed across the screen. Effect after operation:

Now let’s get it moving!

Animation gallery

Image.layer. transform = imageTransform add:

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)
Copy the code

This code is very familiar: Create a layer animation on the Transform property and set it from its current value to the imageTransform you designed earlier. After running, click the “Browse” button, the effect is as follows:

You have now completed the gallery; When you add the ability to turn off the fan when the user clicks the Browse button, you will revisit it in the Challenges section.

A little bit more interaction

Add a bit of interactivity to the image library: Click on the image, make it full screen, and move it to the front so the user can see it better.

ImageViewCard already has a closure expression property called didSelect, and when the user clicks on an image, the clicked image view is given to the closure as an input parameter.

First add the following code to the for body of viewDidAppear() :

image.didSelect = selectImage
Copy the code

Add methods to ViewController:

func selectImage(selectedImage: ImageViewCard) {

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

        } else{}}}Copy the code

Now you need two more animations: one to animate the selected image and one to animate all the other images in the gallery. You will solve this problem in reverse and fade out the unselected image first.

The above method also lacks two animations. When image === selectedImage is the animation of the selectedImage; Or, animation of all other images not selected, the former code is:

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
})
Copy the code

The latter code is:

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

Here, you don’t 3D transform the animation and then make sure the image is at the top of the view stack so that it is visible.

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

self.navigationItem.title = selectedImage.title
Copy the code

Switch the gallery

This summary will enable the Browse button to close the gallery view.

Add a new isGalleryOpen property to the ViewController and set its initial value to false.

The value of this property needs to be updated in two places in the code:

  • intoggleGallery(_:)Set it totrue
  • inselectImage(selectedImage:)Set it tofalse

At the top of toggleGallery(), add a check to see if the gallery is open. If turned on, all images are iterated over and their conversion is set to the original value. Don’t forget to reset isGalleryOpen and return, so the rest of the method code doesn’t execute either.

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

        let animation = CABasicAnimation(keyPath: "transform")
        animation.fromValue = NSValue(caTransform3D: image.layer.transform)
        animation.toValue = NSValue(caTransform3D: CATransform3DIdentity)
        animation.duration = 0.33

        image.layer.add(animation, forKey: nil)
        image.layer.transform = CATransform3DIdentity

    }

    isGalleryOpen = false
    return
}
Copy the code

Final result of this chapter:

This article is in my personal blog address: system learning iOS animation six: 3D animation