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

View Animations we learned about creating View Animations. This part is about the more powerful and lower-level Core Animation APIs. The name of the core animation can be a bit misleading, but can be interpreted as Layer Animations in the title of this article.

In this part of the book, you’ll learn about animation layers instead of views and how to use special layers.

Layers are a simple model class that exposes a number of properties to represent some image-based content. Each UIView has a layer support (each has a Layer property).

View vs Layers

Layers are different from Views (for animations) for the following reasons:

  • A layer is a model object — it exposes data properties and does not implement any logic. It does not have complex automatic layout dependencies, nor does it handle user interaction.
  • Layers have predefined visible features — these are the many data properties that affect how content appears on the screen, such as border lines, border colors, positions, and shadows.
  • Finally, Core Animation optimizes the cache of layer content and quickly draws directly on the GPU.

Individually, the advantages of both.

View:

  • Complex view hierarchical layout, automatic layout, etc.
  • User interaction.
  • Usually has custom logic or custom drawing code that executes on the main thread on the CPU.
  • Very flexible, powerful, subclasses many classes.

Layer:

  • Simpler hierarchy, faster layout and faster drawing.
  • There is no responder chain overhead.
  • By default there is no custom logic and it is drawn directly on the GPU.
  • Less flexible, less subclassed classes.

View and layer selection tips: View animations can be selected at any time; When higher performance is required, layer animations are used.

Where both are in the architecture:

Preview:

This article is longer with more pictures and warnings ⚠️😀.

8- Layer Animation primer – Start with the simplest layer animation and learn how to debug animation errors. 9- Animation Keys and proxies – how to better control the animation currently running and respond to animation events using proxy methods. 10- Animation groups and Time control – Combine many simple animations and run them together as a group. 11- Layer SpringAnimation – Learn how to create powerful and flexible spring layer animations using CASpringAnimation. 12- Layer keyframe animation and structural Properties – Learn about layer keyframe animation, some special handling of animation structural properties.

Next, learn a few specialized layers:

13- Shapes and Masks – Use CAShapeLayer to draw shapes on the screen and animate their special path properties. 14- Gradient Animation – Learn how to draw gradients and animate gradients using the CAGradientLayer. 15-Stroke and Path Animation – Draw shapes interactively and use some of the power of keyframe animation. 16- Duplicate Animations – Learn how to create multiple copies of layer content and then animate from those copies.

8- Layer animation primer

Layer animation works much like view animation; You simply animate the properties between the start and end values for a defined time period, and then let Core Animation handle rendering between the two.

However, layer animations have more animatable properties than view animations; This provides a lot of choice and flexibility when designing effects; Layer animation also has many specialized CALayer subclasses (such as CATextLayer, CAShapeLayer, CATransformLayer) , CAGradientLayer, CAReplicatorLayer, CAScrollLayer, CAEmitterLayer, AVPlayerLayer, etc.), these subclasses provide many other properties.

This chapter introduces the basics of CALayer and Core Animation.

Animatable property

Can be viewed in contrast to the animatable property of view animation.

Location and size

Bounds, position, transform

edge

BorderColor, borderWidth, cornerRadius

shadow

ShadowOffset: Makes the shadows appear closer to or farther away from the layer. ShadowOpacity: Enables shadows to fade in or out. ShadowPath: Changes the shape of the layer shadow. Different 3D effects can be created to make the layers appear to float over different shadow shapes and positions. ShadowRadius: Control shadow blur; This is especially useful when the simulated view is moving towards or away from the surface where the shadow is being cast.

content

Contents: Modify this to specify raw TIFF or PNG data as layer contents.

Mask: Modify the shape or image that will be used to mask the layer’s visible content. This property is described and used in detail in 13- Shapes and masks.

opacity

First layer animation

Start the project using the 3-transition animation to complete the project.

Replace the head view animation with a layer animation.

Delete viewWillAppear() from ViewController respectively:

heading.center.x    -=  view.bounds.width
Copy the code

And viewDidAppear () :

UIView.animate(withDuration: 0.5) {
     self.heading.center.x += self.view.bounds.width
}
Copy the code

At the beginning of viewWillAppear() (after the super call) add:

let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
flyRight.duration = 0.5 
Copy the code

The animation object in the core animation is just a simple data model; The above code creates an instance of CABasicAnimation and sets some data properties. This example describes a potential layer animation: it can be run now, run later, or not run at all.

Since the animation is not bound to a particular layer, the animation can be reused on other layers, and each layer will run a copy of the animation independently.

In the animation model, you can specify the properties to be set to the animation as keypath parameters (such as “position.x” set above); This is handy because the animation is always set in the layer.

Next, set fromValue and toValue for the properties specified on keypath. I need an animated object (I’m dealing with heading here) from the left side of the screen to the center of the screen. The concept of animation duration has not changed; Duration set to 0.5 seconds.

Now that the GIF is set up, you need to add it to the layer that you want to run the animation on. Add the animation to the heading layer below the code you just added:

heading.layer.add(flyRight, forKey: nil)
Copy the code

Add (_:forKey:) will make a copy of the animation to the layer to be added. If you need to change or stop the animation later, you can add the forKey parameter to identify the animation.

The animation now looks exactly the same as the previous view animation.

More layer animation knowledge

The same method is applied to Username Filed, delete the corresponding codes in viewWillAppear() and viewDidAppear() Username Filed layer = Username Filed layer

username.layer.add(flyRight, forKey: nil)
Copy the code

Running the project at this time will look a little awkward, because the animations of heading Label and Username Filed are the same, and Username Filed has no previous delay effect.

Username Filed layer before adding animation to Username Filed layer

flyRight.beginTime = CACurrentMediaTime() + 0.3
Copy the code

The beginTime property of the animation sets the absolute time at which the animation should begin; In this case, you can use CACurrentMediaTime() to get the current time(an absolute time of the system, the time the machine is turned on, taken from the machine time mach_Absolute_time ()), and add the desired delay in seconds.

At this point, if you look carefully, you will find a problem. Username Filed has appeared before the animation starts, which involves the animation property fillMode of another layer.

aboutfillMode

Use the moving animation of Username Field to see the difference of fillMode values. In order to facilitate observation, I increase the beginTime time, and the code is similar to:

let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.fromValue = -view.bounds.size.width/2
flyRight.toValue = view.bounds.size.width/2
flyRight.duration = 0.5
heading.layer.add(flyRight, forKey: nil)

flyRight.beginTime = CACurrentMediaTime() + 2.3
flyRight.fillMode = kCAFillModeRemoved
username.layer.add(flyRight, forKey: nil)
Copy the code
  • KCAFillModeRemoved is the default value for fillMode

    Start the animation at the defined beginTime (if beginTime is not set, i.e. beginTime equals CACurrentMediaTime(), start the animation immediately) and delete the changes made during the animation when it is complete:

    Actual effect:

    The animation doesn’t start between now and begin, but the Username Field is displayed directly, and then the animation starts at BEGIN, which is what happened before.

  • kCAFillModeBackwards

    KCAFillModeBackwards will immediately display the first frame of the animation on screen and launch the animation later, regardless of the actual start time of the animation:

    Actual results:

    The first frame at fromValue (“position.x”) is negative out of screen, so the Username Field is not visible at the beginning. Wait 2.3 seconds for the animation to start.

  • kCAFillModeForwards

    KCAFillModeForwards plays animations as usual, but keeps the last frame of animations on the screen until you delete the animations:

    Actual effect:

    In addition to setting kcafillmodeforward, some other changes need to be made to the layer to get the last frame “pasted”. You will learn about this later in this chapter. It’s a little bit like the first one, but there’s a difference.

  • kCAFillModeBoth

    KCAFillModeBoth is a combination of kCAFillModeForwards and kCAFillModeForwards; This causes the first frame of the animation to appear on screen immediately, leaving the final frame on screen at the end of the animation:

    Actual effect:

    To resolve previously discovered problems, kCAFillModeBoth will be used.

    For Password Field, delete its view animation code and replace it with a layer animation similar to Username Field, but beginTime is a little later.

    Copy the code

Flyright.begintime = CACurrentMediaTime() + 0.3 flyright.fillmode = kCAFillModeBoth username.layer.add(flyRight, forKey: nil)

Flyright.begintime = CACurrentMediaTime() + 0.4 password.layer.add(flyRight, forKey: nil)

So far, your animation ends exactly where the form element was originally located in the Interface Builder. However, many times this is not the case.### Debug animationContinue to add: after the above animation ` ` ` swift username. Layer. The position. X = the bounds. The width password. Layer. The position. X = the bounds. WidthCopy the code

Flyright.fromvalue = -view.bounds.size. Width /2 (this code can be commented out for now).

Continue with the above code and add a delay function:

delay(seconds: 5.0)
  print("where are the fields?")}Copy the code

And interrupts the point after running:

Enter the UI Hierarchy window:

UI Hierarchy allows you to view the UI hierarchy of the current runtime, including hidden or transparent views and off-screen views. It can also be viewed in 3D.

Of course, you can also view the real-time properties in the detector on the right:

After the animation is complete, code changes cause the field to jump back to its original position. But why?

Animation vs. real content

When you animate a Text Field, you don’t actually see that the Text Field itself is animated; Instead, you’ll see a cached version of it called the Presentation Layer. The presentation Layer will be removed from the screen after the original layer is returned to its original position. First, remember to set the Text Field outside the screen in viewWillAppear(_:) :

When the animation starts, the Text Field is temporarily hidden and a pre-rendered animation object will replace it:

There is now no way to click on the animated object, enter any text, or use any other specific text field functionality, because it is not a real text field, just a visible “phantom”. Once the animation is complete, it will disappear from the screen and the original Text Field will be unhidden. But it’s still on the left side of the screen!

To solve this puzzle, you need to use another CABasicAnimation property: isRemovedOnCompletion.

Setting fillMode to kCAFillModeBoth keeps the animation on screen after completion and displays the first frame of the animation before it begins. To complete the effect, you need to set up removedOnCompletion accordingly, and the combination of the two will make the animation visible on the screen. After setting fillMode, add the following line to viewWillAppear() :

flyRight.isRemovedOnCompletion = false
Copy the code

IsRemovedOnCompletion defaults to true, so the animation will disappear as soon as it completes. Set it to false and combine it with the correct fillMode to keep the animation on screen.

Now run the project and you should see that all elements remain on the screen as expected.

Update the layer model

After removing the layer animation from the screen, the layer will fall back to its current position and other property values. This means that you usually need to update the layer properties to reflect the final value of the animation.

Although I’ve explained how setting isRemovedOnCompletion to false works, avoid using it whenever possible. Keeping animations on the screen affects performance, so you need to automatically remove them and update the location of the original layer.

I need to set the original layer to the middle of the screen, in viewWillAppear midday:

username.layer.position.x = view.bounds.size.width/2
password.layer.position.x = view.bounds.size.width/2
Copy the code

Flyright.fromvalue = -view.bounds.size. Width /2 (flyright.fromValue = -view.bounds.size. Width /2)

Fade in ☁️ using layer animation

Remove the code in viewWillAppear() that sets the four ☁️ opacity to 0.0, and the view animation in viewDidAppear() ☁️.

And then in viewDidAppear() add:

let cloudFade = CABasicAnimation(keyPath: "alpha")
cloudFade.duration = 0.5
cloudFade.fromValue = 0.0
cloudFade.toValue = 1.0
cloudFade.fillMode = kCAFillModeBackwards

cloudFade.beginTime = CACurrentMediaTime() + 0.5
cloud1.layer.add(cloudFade, forKey: nil)

cloudFade.beginTime = CACurrentMediaTime() + 0.7
cloud2.layer.add(cloudFade, forKey: nil)

cloudFade.beginTime = CACurrentMediaTime() + 0.9
cloud3.layer.add(cloudFade, forKey: nil)

cloudFade.beginTime = CACurrentMediaTime() + 1.1
cloud4.layer.add(cloudFade, forKey: nil)
Copy the code

Animation of login button background color change

Change the original login button background color animation to a layer animation.

Delete from logIn() :

self.loginButton.backgroundColor = UIColor(red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0)
Copy the code

Delete from resetForm() :

self.loginButton.backgroundColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
Copy the code

Create a global background color change animation function in the viewController.swift file:

func tintBackgroundColor(layer: CALayer, toColor: UIColor) {
    let tint = CABasicAnimation(keyPath: "backgroundColor")
    tint.fromValue = layer.backgroundColor
    tint.toValue = toColor.cgColor
    tint.duration = 0.5
    layer.add(tint, forKey: nil)
    layer.backgroundColor = toColor.cgColor
}
Copy the code

In logIn() add:

let tintColor = UIColor(red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0)
tintBackgroundColor(layer: loginButton.layer, toColor: tintColor)
Copy the code

Add the following to the Completion closure of the login button animation method in resetForm() :

completion: { _ in
     let tintColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
     tintBackgroundColor(layer: self.loginButton.layer, toColor: tintColor)
})
Copy the code

Rounded corner animation of the login button

Create a global filleted change animation function in the viewController.swift file:

func roundCorners(layer: CALayer, toRadius: CGFloat) {
    let round = CABasicAnimation(keyPath: "cornerRadius")
    round.fromValue = layer.cornerRadius
    round.toValue = toRadius
    round.duration = 0.33
    layer.add(round, forKey: nil)
    layer.cornerRadius = toRadius
}
Copy the code

In logIn() add:

roundCorners(layer: loginButton.layer, toRadius: 25.0)
Copy the code

Add the following to the Completion closure of the login button animation method in resetForm() :

roundCorners(layer: self.loginButton.layer, toRadius: 10.0)
Copy the code

Changes in two states:

The two animation functions, tintBackgroundColor and roundCorners, need to finally assign the animation’s most variable final value to the animation’s property, which corresponds to the previous animation vs Real Content section (# animation vs Real Content)

Final results of this chapter:

9- Animated Keys and agents

One of the tricky things about view animation and the corresponding closure syntax is that once you create and run a view animation, you can’t pause, stop, or access it in any way.

However, with core animations, you can easily inspect animations running on layers and stop them if needed. In addition, you can even set delegate objects on the animation and react to the animation events.

The beginning project of this chapter uses the project completed in the previous chapter

Introduction to animation Agent

CAAnimationDelegate has two proxy methods:

func animationDidStart(_ anim: CAAnimation)
func animationDidStop(_ anim: CAAnimation, finished flag: Bool)
Copy the code

To do a little test, at flyRight initialization, add:

flyRight.delegate = self
Copy the code

Add an extension to the ViewController and implement a proxy method:

extension ViewController: CAAnimationDelegate {
    
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        print(anim.description, "Animation completed")}}Copy the code

Run, print the result:

<CABasicAnimation: 0x6000032376e0> Animation complete <CABasicAnimation: 0x600003237460> Animation complete <CABasicAnimation: 0x600003237480> Animation completeCopy the code

You’ll notice that the animationDidStop(_: Finished 🙂 method is called three times and the animation is different each time. This is because a copy is made each time layer.add(_:forKey:) is called to add an animation to a layer, as explained earlier in layer animation basics.

KVO

The CAAnimation class and its subclasses are written in Objective-C and conform to key-value encoding (KVO), which means you can treat them like dictionaries and add new properties to them at run time. (For KVO, see key/value coding (KVC) in my summary article OC.)

Use this mechanism to specify a name for the flyRight animation so that it can later be identified from other active animations.

After flyright.delegate = self in viewWillAppear() add:

flyRight.setValue("form", forKey: "name")
flyRight.setValue(heading.layer, forKey: "layer")
Copy the code

In the above code, create a key-value pair on the flyRight animation with a key of “name” and a value of “form”, which can be identified from the delegate callback method call;

A key pair with the key “layer” and the value heading. Layer is also created to reference the layer to which the animation belongs later.

The same can be added (as stated earlier, each animation will be copied, so it will not be overwritten) :

flyRight.setValue(username.layer, forKey: "layer")

// ...

flyRight.setValue(password.layer, forKey: "layer")
Copy the code

Verify the above code in the proxy callback method and add a simple pulsation animation after the above movement animation is over:

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    // print(anim.description, "animation done ")
    guard let name = anim.value(forKey: "name") as? String else {
        return
    }

    if name == "form" {
        // 'value(forKey:)' always results in 'Any', so it needs to be converted to the desired type
        let layer = anim.value(forKey: "layer") as? CALayer
        anim.setValue(nil, forKey: "layer")
        // Simple pulse animation
        let pulse = CABasicAnimation(keyPath: "transform.scale")
        pulse.fromValue = 1.25
        pulse.toValue = 1.0
        pulse.duration = 0.25layer? .add(pulse, forKey:nil)}}Copy the code

Note: layer? .add() means that the add(_:forKey:) call will be skipped if no layers are stored in the animation. This is Optional Chaining in Swift. Learn SWIFt-17 as an Optional Chaining code.

After the move animation ends, there is a simple larger pulsation animation effect:

Animation Keys

The add(_:forKey:) parameter forKey(not to be confused with the setValue(_:forKey:) parameter forKey) has not been used before.

In this section, you’ll create another layer animation, learn how to run multiple animations at once, and learn how to use animation Keys to control running animations.

Add a new TAB that will animate slowly from right to left to prompt the user for input. Once the user starts entering their username or password (the Text Field gets focus), the label will stop moving and jump straight to its final position (center). There is no need to continue animation once the user knows what to do.

Add the property let info = UILabel() in ViewController and configure it in viewDidLoad() :

info.frame = CGRect(x: 0.0, y: loginButton.center.y + 60.0,  width: view.frame.size.width, height: 30)
info.backgroundColor = UIColor.clear
info.font = UIFont(name: "HelveticaNeue", size: 12.0)
info.textAlignment = .center
info.textColor = UIColor.white
info.text = "Tap on a field and enter username and password"
view.insertSubview(info, belowSubview: loginButton)
Copy the code

Add two animations for info:

// Prompts two animations of the Label
let flyLeft = CABasicAnimation(keyPath: "position.x")
flyLeft.fromValue = info.layer.position.x + view.frame.size.width
flyLeft.toValue = info.layer.position.x
flyLeft.duration = 5.0
info.layer.add(flyLeft, forKey: "infoappear")

let fadeLabelIn = CABasicAnimation(keyPath: "opacity")
fadeLabelIn.fromValue = 0.2
fadeLabelIn.toValue = 1.0
fadeLabelIn.duration = 4.5
info.layer.add(fadeLabelIn, forKey: "fadein")
Copy the code

FlyLeft is the animation that moves from left to right, and fadeLabelIn is the animation that grows in transparency.

The animation looks like this:

Add a proxy for the Text Field. By extending the ViewController to follow the UITextFieldDelegate protocol:

extension ViewController: UITextFieldDelegate {
    func textFieldDidBeginEditing(_ textField: UITextField) {
        guard let runningAnimations = info.layer.animationKeys() else {
            return
        }
        print(runningAnimations)
    }
}
Copy the code

In viewDidAppear() add:

username.delegate = self
password.delegate = self
Copy the code

Click on the text box while the info animation is still in progress to print the animation key:

["infoappear"."fadein"]
Copy the code

Add to textFieldDidBeginEditing(:) :

info.layer.removeAnimation(forKey: "infoappear")
Copy the code

After clicking the textbox, delete the animation moving from left to right, and the info immediately reaches the end point, in the center of the screen:

It is also possible to removeAllAnimations from the layer with the removeAllAnimations() method.

** note: the animation is removed from the layer by default, so the animationKeys() method will no longer have the animationKeys.

Modify the animation at ☁️

Modify the animation of ☁️ by using the animation agent and animation KVO learned in this chapter

First add the animation method to the ViewController:

/// Cloud layer animation
func animateCloud(layer: CALayer) {
    let cloudSpeed = 60.0 / Double(view.layer.frame.size.width)
    let duration: TimeInterval = Double(view.layer.frame.size.width - layer.frame.origin.x) * cloudSpeed
    
    let cloudMove = CABasicAnimation(keyPath: "position.x")
    cloudMove.duration = duration
    cloudMove.toValue = self.view.bounds.width + layer.bounds.width/2
    cloudMove.delegate = self
    cloudMove.setValue("cloud", forKey: "name")
    cloudMove.setValue(layer, forKey: "layer")
    layer.add(cloudMove, forKey: nil)}Copy the code

Replace the four animateCloud method calls in viewDidAppear() with:

animateCloud(layer: cloud1.layer)
animateCloud(layer: cloud2.layer)
animateCloud(layer: cloud3.layer)
animateCloud(layer: cloud4.layer)
Copy the code

To keep ☁️ moving, add the following to the animationDidStop method:

if name == "cloud" {
    if let layer = anim.value(forKey: "layer") as? CALayer {
        anim.setValue(nil, forKey: "layer")
        
        layer.position.x = -layer.bounds.width/2
        delay(0.5) {
            self.animateCloud(layer: layer)
        }
    }
}
Copy the code

Effects of this chapter:

10- Animation group and time control

In the previous chapter, you learned how to add multiple independent animations to a single layer. But what if you want your animations to work synchronously and to be consistent with each other? This is where animation groups come in.

This chapter shows you how to group animations using CAAnimationGroup. You can add multiple animations to a group and adjust properties like duration, delegate, and timingFunction at the same time. Grouping the animations results in simplified code and ensures that all of your animations will be synchronized as a single entity unit.

The beginning project of this chapter uses the project completed in the previous chapter

CAAnimationGroup

Delete from viewWillAppear() :

loginButton.center.y += 30.0
loginButton.alpha = 0.0
Copy the code

Delete the display animation of the login button in viewDidAppear() :

UIView.animate(withDuration: 0.5, delay: 0.5, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: [], animations: {
    self.loginButton.center.y -= 30.0
    self.loginButton.alpha = 1.0
}, completion: nil)
Copy the code

In viewDidAppear() add the group animation:

let groupAnimation = CAAnimationGroup()
groupAnimation.beginTime = CACurrentMediaTime() + 0.5
groupAnimation.duration = 0.5
groupAnimation.fillMode = kCAFillModeBackwards 
Copy the code

CAAnimationGroup inherits from CAAnimation. It also has beginTime, Duration, fillMode, delegate, and other properties.

Continue with the three animations and add them to the group animation above:

let scaleDown = CABasicAnimation(keyPath: "transform.scale")
scaleDown.fromValue = 3.5
scaleDown.toValue = 1.0

let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = .pi / 4.0
rotate.toValue = 0.0

let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = 0.0
fade.toValue = 1.0

groupAnimation.animations = [scaleDown, rotate, fade]
loginButton.layer.add(groupAnimation, forKey: nil)
Copy the code

The login button works like this:

Animation slow

Animation easing in layer animation is conceptually the same as the animation options in view animation introduced in 1- View Animation Introduction, but the syntax is different.

Animation easing in layer animation is represented by the CAMediaTimingFunction class. Usage:

groupAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
Copy the code

The name parameter has the following types, similar to those in view animation:

KCAMediaTimingFunctionLinear speed does not change

At the end of the kCAMediaTimingFunctionEaseIn started slow, fast

At the end of the kCAMediaTimingFunctionEaseOut faster at the beginning, slowly

KCAMediaTimingFunctionEaseInEaseOut start end are slow, fast in the middle

You can try different effects.

Init (controlPoints c1x: Float, _ c1y: Float, _ c2x: Float, _ c2y: Float, _ c2y: Float, Float), you can customize the slow mode, refer to the official documentation

More options for animation time control

Repetition of the animation

RepeatCount Sets the number of times the animation is repeated. To add the number of repetitions to the animation of the prompt Label, set the flyLeft animation property in viewDidAppear() :

flyLeft.repeatCount = 4
Copy the code

Another repeatDuration can be used to set the total repeat time.

As with view animations, there is an Autoreverses that is otherwise incoherent:

flyLeft.autoreverses = true
Copy the code

It looks good now, but there’s a problem. After 4 repetitions, it jumps straight to the center of the screen like this (the GIF has omitted the first few scrolling because it’s too long) :

It makes sense, too, that the last loop ends with the tag leaving the screen. The solution is half an animation cycle:

flyLeft.repeatCount = 2.5
Copy the code

Change the speed of the animation

The speed of an animation can be controlled independently of the duration by setting the speed property.

flyLeft.speed = 2.0
Copy the code

Change the animations of the three forms to animate groups

The following code:

    let flyRight = CABasicAnimation(keyPath: "position.x")
    flyRight.fromValue = -view.bounds.size.width/2
    flyRight.toValue = view.bounds.size.width/2
    flyRight.duration = 0.5
    flyRight.fillMode = kCAFillModeBoth
    flyRight.delegate = self
    flyRight.setValue("form", forKey: "name")
    flyRight.setValue(heading.layer, forKey: "layer")
    
    heading.layer.add(flyRight, forKey: nil)
    
    flyRight.setValue(username.layer, forKey: "layer")
    
    flyRight.beginTime = CACurrentMediaTime() + 0.3
    username.layer.add(flyRight, forKey: nil)
    
    flyRight.setValue(password.layer, forKey: "layer")
    
    flyRight.beginTime = CACurrentMediaTime() + 0.4
    password.layer.add(flyRight, forKey: nil)
Copy the code

Is amended as:

    let formGroup = CAAnimationGroup()
    formGroup.duration = 0.5
    formGroup.fillMode = kCAFillModeBackwards
    
    let flyRight = CABasicAnimation(keyPath: "position.x")
    flyRight.fromValue = -view.bounds.size.width/2
    flyRight.toValue = view.bounds.size.width/2
    
    let fadeFieldIn = CABasicAnimation(keyPath: "opacity")
    fadeFieldIn.fromValue = 0.25
    fadeFieldIn.toValue = 1.0
    
    formGroup.animations = [flyRight, fadeFieldIn]
    heading.layer.add(formGroup, forKey: nil)
    
    formGroup.delegate = self
    formGroup.setValue("form", forKey: "name")
    formGroup.setValue(username.layer, forKey: "layer")
    
    formGroup.beginTime = CACurrentMediaTime() + 0.3
    username.layer.add(formGroup, forKey: nil)
    
    formGroup.setValue(password.layer, forKey: "layer")
    formGroup.beginTime = CACurrentMediaTime() + 0.4
    password.layer.add(formGroup, forKey: nil)
Copy the code

Final results of this chapter:

11- Layer spring animation

The 2-spring animation in the previous view animation can be used to create some relatively simple spring-like animations, while the Layer Springs animation studied in this section can present a more natural looking physical simulation.

Start this chapter with a project that uses the project completed in the previous chapter, adding some new layers for spring animations and explaining the differences between the two kinds of spring animations.

A few theories:

Damped harmonic oscillator

Damping oscillators, Damped harmonic oscillators, can be understood as oscillations that decay gradually.

The UIKit API simplifies the creation of spring animations, and you don’t need to know how they work to make them easy to use. However, since you are now a core animation expert, you need to delve into the details.

A pendulum, ideally a pendulum swings constantly, like the following:

The corresponding motion trajectory diagram looks like:

In reality, however, the pendulum swings less and less because of the loss of energy:

Corresponding trajectory:

This is a damped harmonic oscillator.

The length of time it takes for the pendulum to stop, and the manner in which the final oscillator shapes depend on the following parameters of the oscillating system:

  • Damping: External decelerating forces acting on the system due to air friction, mechanical friction, and other forces.

  • Mass: The heavier the pendulum, the longer the swinging time.

  • Stiffness: The harder the oscillator’s “spring” (the pendulum’s “spring” refers to the earth’s gravity), the harder the pendulum swings, the faster the system stops. Imagine using this pendulum on the moon or Jupiter; The motion in low gravity and high gravity will be completely different.

  • Initial Velocity: Push the pendulum.

“It’s all very interesting, but what does the spring animation have to do with it?”

The damping resonance subsystem is what drives the spring animation in iOS. The next section discusses this in more detail.

View spring animation vs Layer spring animation

UIKit adjusts all other variables dynamically to stabilize the system for a given duration. That’s why UIKit spring animations sometimes feel a little forced to stop. UIKit animation is a little bit unnatural if you look at it.

Fortunately, the core allows you to create appropriate spring animations for layer properties through the CASpringAnimation class. CASpringAnimation creates the spring animation for UIKit behind the scenes, but when we call it directly, we can set various system variables to make the animation stabilize itself. The disadvantage of this approach is that you cannot set a fixed duration; The duration depends on the other variables provided and is calculated by the system.

Some properties of CASpringAnimation (corresponding to the parameters of the previous oscillation system) :

Damping coefficient, preventing spring expansion coefficient, the greater the damping coefficient, the faster the stop

Mass affects the inertia of the spring when the layer is moving. The greater the mass, the greater the range of spring stretching and compression

The larger the stiffness factor, the greater the force generated by the deformations and the faster the movement

InitialVelocity The initial speed of the animated view. When the speed is positive, the direction of velocity is consistent with the direction of motion; when the speed is negative, the direction of velocity is opposite to the direction of motion

The first layer spring animation

The BahamaAirLoginScreen project has a pulsing animation after the two textbox movement animations, letting the user know that the field is active and ready to use. However, the animation ends abruptly. Make the pulse animation more natural by using CASpringAnimation.

AnimationDidStop (_:finished:)

// Simple pulse animation
let pulse = CABasicAnimation(keyPath: "transform.scale")
pulse.fromValue = 1.25
pulse.toValue = 1.0
pulse.duration = 0.25layer? .add(pulse, forKey:nil)
Copy the code

To:

let pulse = CASpringAnimation(keyPath: "transform.scale")
pulse.damping = 2.0
pulse.fromValue = 1.25
pulse.toValue = 1.0pulse.duration = pulse.settlingDuration layer? .add(pulse, forKey:nil)
Copy the code

Before and after comparison of effect pictures:

Notice duration here. Pulse.settlingduration Is the time from start to end of the spring animation that the system estimates based on the current parameter.

The spring system does not stabilize in 0.25 seconds; The supplied variables mean that the animation should run a little longer before it stops. A visual demonstration of how to cut the spring animation:

If the jitter time is too long, damping coefficient damping can be increased, such as Pulse.damping = 7.5.

Spring animation properties

The default values for the predefined spring animation properties of CASpringAnimation are:

Damping: 10.0 MASS: 1.0 Stiffness: 100.0 initialVelocity: 0.0Copy the code

A proxy method to implement a text box:

func textFieldDidEndEditing(_ textField: UITextField) {
    guard let text = textField.text else {
        return
    }
    if text.count < 5 {
        let jump = CASpringAnimation(keyPath: "position.y")
        jump.fromValue = textField.layer.position.y + 1.0
        jump.toValue = textField.layer.position.y
        jump.duration = jump.settlingDuration
        textField.layer.add(jump, forKey: nil)}}Copy the code

The above code indicates that when the user finishes typing in the text, if the number of characters is less than 5, there will be a small amplitude of jitter animation to remind the user that it is too short.

initialVelocity

Start speed. Default is 0.

Jump. duration = jump.settlingDuration:

jump.initialVelocity = 100.0
Copy the code

Effect:

The text box popped higher due to the extra push at the beginning.

mass

Increasing the initial speed will make the animation last longer. What about increasing the mass?

Jump. InitialVelocity = 100.0

jump.mass = 10.0
Copy the code

Effect:

The extra mass makes the textbox jump higher and stabilize for longer.

stiffness

Stiffness, the default is 100. The bigger the spring, the harder it is.

Jum. mass = 10.0 add:

jump.stiffness = 1500.0
Copy the code

Effect:

Now the jumps are not so high.

damping

The animation looks great, but it does seem a little too long. Increase system damping to stabilize the animation faster.

Add after jumping. = 1500.0:

jump.damping = 50.0
Copy the code

Effect:

Special Layer properties

Add a colored border when the text box shakes.

Add (jump, forKey: nil) to textField.layer.add(jump, forKey: nil) :

textField.layer.borderWidth = 3.0
textField.layer.borderColor = UIColor.clear.cgColor
Copy the code

This code adds a transparent border around the text box. After the above code add:

let flash = CASpringAnimation(keyPath: "borderColor")
flash.damping = 7.0
flash.stiffness = 200.0
flash.fromValue = UIColor(red: 1.0, green: 0.27, blue: 0.0, alpha: 1.0).cgColor
flash.toValue = UIColor.white.cgColor
flash.duration = flash.settlingDuration
textField.layer.add(flash, forKey: nil)
Copy the code

Run, slow down effect:

Note: In some iOS versions, layer animations remove rounded corners from text fields. This situation can be added after the last piece of code on the trip: textField. Layer. The cornerRadius = 5.

Turn the rounded corners and background color change animation of the login button into an elastic animation

This change is convenient by simply modifying two functions in viewController.swift:

// Layer animation of background color change
func tintBackgroundColor(layer: CALayer, toColor: UIColor) {
// let tint = CABasicAnimation(keyPath: "backgroundColor")
// tint.fromValue = layer.backgroundColor
// tint.toValue = toColor.cgColor
/ / tint. Duration = 0.5
// layer.add(tint, forKey: nil)
// layer.backgroundColor = toColor.cgColor
    
    let tint = CASpringAnimation(keyPath: "backgroundColor")
    tint.damping = 5.0
    tint.initialVelocity = -10.0
    tint.fromValue = layer.backgroundColor
    tint.toValue = toColor.cgColor
    tint.duration = tint.settlingDuration
    layer.add(tint, forKey: nil)
    layer.backgroundColor = toColor.cgColor
    
    
}
// Rounded corner animation
func roundCorners(layer: CALayer, toRadius: CGFloat) {
// let round = CABasicAnimation(keyPath: "cornerRadius")
// round.fromValue = layer.cornerRadius
// round.toValue = toRadius
/ / round. Duration = 0.33
// layer.add(round, forKey: nil)
// layer.cornerRadius = toRadius
    
    let round = CASpringAnimation(keyPath: "cornerRadius")
    round.damping = 5.0
    round.fromValue = layer.cornerRadius
    round.toValue = toRadius
    round.duration = round.settlingDuration
    layer.add(round, forKey: nil)
    layer.cornerRadius = toRadius
}
Copy the code

12- Layer keyframe animation and structural properties

Layer Keyframe Animations (CAKeyframeAnimation) on layers are slightly different from those on UIView. View keyframe animation is a combination of individual simple animations that can be set for different views and properties. The animations can overlap or have gaps between them.

In contrast, CAKeyframeAnimation allows us to animate a single property on a given layer. Different key points of the animation can be defined, but there must be no gaps or overlaps in the animation. Although it may sound restrictive, you can use CAKeyframeAnimation to create some pretty dramatic effects.

In this chapter, you will create many layer keyframe animations, ranging from very basic simulated real world collisions to more advanced animations. In 15-Stroke and Path Animation, you’ll learn how to get a layer animation further and animate a layer along a given path.

Now you’ll walk before running and create a funky swing effect for your first layer keyframe animation.

Introduces layer keyframe animation

Think about how basic animation works. With fromValue and toValue, the core animation gradually modifies specific layer properties between these values for a specified duration. For example, when rotating the layer between 45° and -45° (or PI / 4 and – PI / 4), only these two values need to be specified, and then the layer renders all intermediate values to complete the animation:

CAKeyframeAnimation uses a set of values to animate instead of fromValue and toValue. You also need to provide the time at which the animation should reach the key point for each value.

In the animation above, the layer was rotated from 45° to -45°, but this time it had two separate stages:

First, it rotates from 45° to 22° for the first two-thirds of the duration of the animation, then it rotates all the way to -45° for the rest of the time. Essentially, setting up an animation with keyframes requires us to provide key values for setting the properties of the animation and a corresponding amount of relative critical time between 0.0 and 1.0.

The beginning project of this chapter uses the project completed in the previous chapter

Create layer keyframe animation

In resetForm() add:

let wobble = CAKeyframeAnimation(keyPath: "transform.rotation")
wobble.duration = 0.25
wobble.repeatCount = 4
wobble.values = [0.0, -.pi/4.0.0.0, .pi/4.0.0.0]
wobble.keyTimes = [0.0.0.25.0.5.0.75.1.0]
heading.layer.add(wobble, forKey: nil)
Copy the code

KeyTimes is a series of values from 0.0 to 1.0 and corresponds to values. After the login button is restored, the heading has a wobbly effect:

Sharp-eyed readers may have noticed that I haven’t covered animation of structural properties. Most of the time, you can dispense with the individual components of the animation structure, such as CGPoint’s X component, or CATransformation3D’s rotation component, but then you’ll find that the animation of the dynamic structure values takes a second thought than you might.

Animating struct values

Structures are first-class citizens in Swift. In fact, there is little difference in syntax between using classes and structures. (Learn SWIFt-9: Classes and Structures in the form of simplified code.) However, the core animation is an Objective-C framework built on C, which means that Structures are handled very differently from Swift Structures. Objective-c apis like to work with objects, so structures need some special handling. This is why it is relatively easy to animate layer properties such as colors or numbers, but not so easy to animate structural properties such as CGPoint. CALayer has many animatable properties that contain struct values, including positions of type CGPoint, conversions of type CATransform3D, and boundaries of type CGRect.

To solve this problem, Cocoa uses the NSValue class, which “wraps” a struct value into an object that core animation can handle.

NSValue comes with a number of handy initializers:

init(cgPoint: CGPoint)
init(cgSize: CGSize)
init(cgRect rect: CGRect)
init(caTransform3D: CATransform3D)
Copy the code

Using examples, here is a sample position animation using CGPoint:

let move = CABasicAnimation(keyPath: "position")
move.duration = 1.0
move.fromValue = NSValue(cgPoint: CGPoint(x: 100.0, y: 100.0))
move.toValue = NSValue(cgPoint: CGPoint(x: 200.0, y: 200.0))
Copy the code

Before assigning cgPoints to fromValue or toValue, you need to convert cgPoints to NSValue, otherwise the animation won’t work. The same goes for keyframe animation.

Hot air balloon keyframe animation

In logIn() add:

let balloon = CALayer()
balloon.contents = UIImage(named: "balloon")! .cgImage balloon.frame =CGRect(x: -50.0, y: 0.0, width: 50.0, height: 65.0)
view.layer.insertSublayer(balloon, below: username.layer)
Copy the code

The insertSublayer(_:below) method creates an image layer as a child of the View.layer.

If you need to display images on the screen but don’t need all the benefits of UIView (such as automatic layout constraints, additional gesture recognizers, etc.), you can simply use the CALayer in the code example above.

Add the animation code after the above code:

let flight = CAKeyframeAnimation(keyPath: "position")
flight.duration = 12.0
flight.values = [
  CGPoint(x: -50.0, y: 0.0),
  CGPoint(x: view.frame.width + 50.0, y: 160.0),
  CGPoint(x: -50.0, y: loginButton.center.y)
].map { NSValue(cgPoint: $0) }

flight.keyTimes = [0.0.0.5.1.0]
Copy the code

The three corresponding points of VALUES are as follows:

Finally add the animation to the balloon layer and set the final position of the balloon layer:

balloon.add(flight, forKey: nil)
balloon.position = CGPoint(x: -50.0, y: loginButton.center.y)
Copy the code

Operation, effect:

13- Shapes and masks

This chapter learns about CAShapeLayer, a subclass of CALayer, which can draw various shapes on the screen, from very simple to very complex.

The chapter’s opening project, MultiplayerSearch, simulates the start screen of a fighting game that is searching for online opponents. One of the view controllers displays a nice background image, some labels, a “Search Again” button (which is transparent by default), and two avatar images, one of which will be empty until the app “finds” an opponent.

Head portrait view

Both avatars are instances of the AvatarView class. Let’s start to complete some of the avatar views. Open avatarView.swift and you’ll find several defined properties that represent:

PhotoLayer: Image layer for your head. CircleLayer: Used to draw the shape layer of the circle. MaskLayer: Another shape layer used to draw masks. Label: The label that displays the player’s name.

The above components already exist in the project but have not yet been added to the view, so the first task is to add them to the active view. Add the following code to didMoveToWindow() :

photoLayer.mask = maskLayer
Copy the code

This simply disguises the square image with circles in the maskLayer.

You can also see the set properties in the storyboard via @ibDesignable (for @ibDesignable, see iOS Tutorial 8: Customizing the UI using IBInspectable and IBDesignable).

Operation effect:

Now add the round border layer to the head view layer and add the code in didMoveToWindow() :

layer.addSublayer(circleLayer)
Copy the code

In this case, the effect is:

Add a name tag:

addSubview(label)
Copy the code

Rebound in the animation

Let’s create a bounce-off animation that looks like two objects colliding and then bouncing off.

Create the searchForOpponent() function in ViewController and call it in viewDidAppear:

func searchForOpponent(a) {
    let avatarSize = myAvatar.frame.size
    let bounceXOffset: CGFloat = avatarSize.width/1.9
    let morphSize = CGSize(width: avatarSize.width * 0.85, height: avatarSize.height * 1.1)}Copy the code

BounceXOffset is the horizontal distance that should be moved when bouncing off each other.

MorphSize is the size of the heads when they collide (width becomes smaller, length becomes larger).

In searchForOpponent(), continue to add:

let rightBouncePoint = CGPoint(x: view.frame.size.width/2.0 + bounceXOffset, y: myAvatar.center.y)
let leftBouncePoint = CGPoint(x: view.frame.size.width/2.0 - bounceXOffset, y: myAvatar.center.y)

myAvatar.bounceOff(point: rightBouncePoint, morphSize: morphSize)
opponentAvatar.bounceOff(point: leftBouncePoint, morphSize: morphSize)
Copy the code

In the bounceOff(Point :morphSize:) method above, the two parameters represent the position of the avatar and the size of the morphing. In AvatarView add:

func bounceOff(point: CGPoint, morphSize: CGSize) {
    let originalCenter = center

    UIView.animate(withDuration: animationDuration, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, animations: {
        self.center = point
    }, completion: {_ in

                   })

    UIView.animate(withDuration: animationDuration, delay: animationDuration, usingSpringWithDamping: 0.7, initialSpringVelocity: 1.0, animations: {
        self.center = originalCenter
    }) { (_) in
        delay(seconds: 0.1) {
            self.bounceOff(point: point, morphSize: morphSize)
        }
       }
}
Copy the code

The two animations above are the use of spring animation to move the avatar to the specified position and the use of spring animation to move the avatar to the original position. The effect is as follows:

Image deformation

In real life, when two objects collide, there is a short pause and the object deforms (the “squashing” effect). Let’s do this.

Add to bounceOff(point:morphSize:) :

let morphedFrame = (originalCenter.x > point.x) ?
        CGRect(x: 0.0, y: bounds.height - morphSize.height, width: morphSize.width, height: morphSize.height) :
        CGRect(x: bounds.width - bounds.width, y: bounds.height - morphSize.height, width: morphSize.width, height: morphSize.height)
Copy the code

Check originalCenter.x > point.x to determine the left or right avatar.

Add more on bounceOff(point:morphSize:) :

let morphAnimation = CABasicAnimation(keyPath: "path")
morphAnimation.duration = animationDuration
morphAnimation.toValue = UIBezierPath(ovalIn: morphedFrame).cgPath

morphAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)

circleLayer.add(morphAnimation, forKey: nil)
Copy the code

Create ellipse with UIBezierPath.

After running, the effect is a little bit wrong:

Only the border layer is distorted, the image layer is unchanged.

Add the morphAnimation to the mask layer:

maskLayer.add(morphAnimation, forKey: nil)
Copy the code

This works much better:

Search rival

Add delay(seconds: 4.0, completion: foundOppoent) to searchForOppoent() and then add to ViewController:

func foundOpponent(a) {
    status.text = "Connecting..."

    opponentAvatar.image = UIImage(named: "avatar-2")
    opponentAvatar.name = "Andy"
}
Copy the code

Use delay to simulate finding an opponent.

Add delay(seconds: 4.0, completion: connectedToOpponent) to foundOpponent() and then add to ViewController:

func connectedToOpponent(a) {
    myAvatar.shouldTransitionToFinishedState = true
    opponentAvatar.shouldTransitionToFinishedState = true
}
Copy the code

ShouldTransitionToFinishedState is custom attributes in AvatarView, used to determine whether the connection is complete, use below.

Add delay(seconds: 1.0, completion: completed) to connectedToOpponent() and then add to ViewController:

func completed(a) {
    status.text = "Ready to play"
    UIView.animate(withDuration: 0.2) {
        self.vs.alpha = 1.0
        self.searchAgain.alpha = 1.0}}Copy the code

After the opponent is found, modify the status language and display the re – search button.

Effect:

When the connection is successful, the head becomes a square

Add a property var isSquare = false to AvatarView to determine if the avatar needs to be converted to a square.

Add the following to the completion closure of bounceOff’s (point:morphSize:) first animation (where the head moves to the specified position) :

if self.shouldTransitionToFinishedState {
    self.animateToSquare()
}
Copy the code

Where animateToSquare() is:

// Transform to a square animation
func animateToSquare(a) {
    isSquare = true

    let squarePath = UIBezierPath(rect: bounds).cgPath
    let morph = CABasicAnimation(keyPath: "path")
    morph.duration = 0.25
    morph.fromValue = circleLayer.path
    morph.toValue = squarePath

    circleLayer.add(morph, forKey: nil)
    maskLayer.add(morph, forKey: nil)

    circleLayer.path = squarePath
    maskLayer.path = squarePath

}
Copy the code

Add a judgment to the completion closure of bounceOff’s (point:morphSize:) second animation (where the head moves to the original position) :

if !self.isSquare {
    self.bounceOff(point: point, morphSize: morphSize)
}
Copy the code

The net effect is:

14- Gradient animation

In this chapter, learn about Gradient Animations using the previous iOS screen “slide to unlock” effects.

The start project SlideToReveal is a simple single-page project with only a UILabel to display the time and a custom UIView subclass AnimateMaskLabel for gradient animation.

The first gradient layer

CAGradientLayer is another subclass of CALayer for gradient layers.

Configure the CAGradientLayer and add to the function block defined by the attribute gradientLayer:

gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
Copy the code

This defines the direction of the gradient and its beginning and end.

    let gradientLayer: CAGradientLayer = {
        let gradientLayer = CAGradientLayer()     
        gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
        gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
        ...   
    }()
Copy the code

This means that the function is called directly after it is defined, and the return value is given directly to the property. This notation is also common in other languages, such as JS.

Continue to add:

let colors = [
    UIColor.black.cgColor,
    UIColor.white.cgColor,
    UIColor.black.cgColor
]
gradientLayer.colors = colors
let locations: [NSNumber] = [0.25.0.5.0.75]
gradientLayer.locations = locations
Copy the code

The above definition is similar to values and keyTimes in the layer keyframe animation we learned earlier.

The result is a gradient that starts with black, ends with white, and ends with black. Locations specify exactly where these colors should appear during the gradient. Of course, it can also have many color points, and corresponding position points.

It looks something like this:

Define the gradient layer frame in layoutSubviews() :

gradientLayer.frame = bounds
layer.addSublayer(gradientLayer)
Copy the code

This defines the gradient layer under AnimateMaskLabel.

Animate the gradient layer

In didMoveToWindow() add:

let gradientAnimation = CABasicAnimation(keyPath: "locations")
gradientAnimation.fromValue = [0.0.0.0.0.25]
gradientAnimation.toValue = [0.75.1.0.1.0]
gradientAnimation.duration = 3.0
gradientAnimation.repeatCount = .infinity
gradientLayer.add(gradientAnimation, forKey: nil)
Copy the code

RepeatCount is set to infinity and the animation lasts 3 seconds and will repeat forever. The effect is as follows:

Locations [0.0, 0.0, 0.25] and [0.75, 1.0, 1.0], which are the start and end points of the animation.

The effect of animation is the state of the former to the state of the latter, so it is easy to understand.

This looks nice, but the gradient width is a bit small. Just zoom in on the gradient boundaries to get a gentler gradient. Find gradientLayer.frame = bounds line in layoutSubviews() instead:

gradientLayer.frame = CGRect(x: -bounds.size.width, y: bounds.origin.y, width: 3 * bounds.size.width, height: bounds.size.height)
Copy the code

This sets the gradient box to three times the width of the visible area. Animate into view, go straight through it, and exit from the right:

Effect:

Create a text mask

Create a text attribute in AnimateMaskLabel:

let textAttributes: [NSAttributedString.Key: Any] = {
    let style = NSMutableParagraphStyle()
    style.alignment = .center
    return [
        NSAttributedString.Key.font: UIFont(name: "HelveticaNeue-Thin", size: 28.0)! .NSAttributedString.Key.paragraphStyle: style
    ]
}()
Copy the code

Next, you need to render the text as an image. Add the following code after setNeedsDisplay() in the property observer of the text property:

let image = UIGraphicsImageRenderer(size: bounds.size).image { (_) in
        text.draw(in: bounds, withAttributes: textAttributes)
}
Copy the code

In this case, the image renderer is used to set the context.

Use this image to create a mask on the gradient layer and continue with the code above:

let maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.frame = bounds.offsetBy(dx: bounds.size.width, dy: 0)
maskLayer.contents = image.cgImage
gradientLayer.mask = maskLayer
Copy the code

Now effect:

Sliding gesture

In viewDidLoad() add:

let swipe = UISwipeGestureRecognizer(target: self, action: #selector(ViewController.didSlide))
        swipe.direction = .right
        slideView.addGestureRecognizer(swipe)
Copy the code

Effect:

Color gradient

Change the colors and locations of the gradient layer so that the black and white layers become colors:

let colors = [
    UIColor.yellow.cgColor,
    UIColor.green.cgColor,
    UIColor.orange.cgColor,
    UIColor.cyan.cgColor,
    UIColor.red.cgColor,
    UIColor.yellow.cgColor
]
Copy the code
let locations: [NSNumber] = [0.0.0.0.0.0.0.0.0.0.0.25]
Copy the code

And modify the fromValue and toValue of the animation:

gradientAnimation.fromValue = [0.0.0.0.0.0.0.0.0.0.0.25]
gradientAnimation.toValue = [0.65.0.8.0.85.0.9.0.95.1.0]
Copy the code

Effect:

Final results of this chapter:

15-Stroke and path animation

Note: stroke can be translated as a stroke, but seems inappropriate, so it is not translated at all 😏.

Start project PullToRefresh

I have a TableView, and I pull down the new view and it stays visible for four seconds, and then it shrinks back. This chapter is to animate a daisy-like turn in this drop-down view.

Create interactive Stroke animations

The first step in building the animation is to create a circle. Open refreshView.swift and add the following code to init(frame:scrollView:) :

// Plane move route layer
ovalShapeLayer.strokeColor = UIColor.white.cgColor
ovalShapeLayer.fillColor = UIColor.clear.cgColor
ovalShapeLayer.lineWidth = 4.0
ovalShapeLayer.lineDashPattern = [2.3]

let refreshRadius = frame.size.height/2 * 0.8

ovalShapeLayer.path = UIBezierPath(ovalIn: CGRect(x: frame.size.width/2 - refreshRadius, y: frame.size.height/2 - refreshRadius, width: 2 * refreshRadius, height: 2 * refreshRadius)).cgPath
layer.addSublayer(ovalShapeLayer)
Copy the code

OvalShapeLayer is a RefreshView property of type CAShapeLayer. CAShapeLayer learned this before, but here, just set the stroke and fill color, and set the circle diameter to 80% of the height of the view to ensure a comfortable margin.

The lineDashPattern property sets the dashed line pattern, which is an array containing the length of the dash and the length of the gap (in pixels), as well as a variety of dashed lines.

In redrawFromProgress() add:

ovalShapeLayer.strokeEnd = progress
Copy the code

To add the airplane image to the airplane layer, init(frame:scrollView:) :

// Add planes
let airplaneImage = UIImage(named: "airplane.png")!
airplaneLayer.contents = airplaneImage.cgImage
airplaneLayer.bounds = CGRect(x: 0.0, y: 0.0, width: airplaneImage.size.width, height: airplaneImage.size.height)
airplaneLayer.position = CGPoint(x: frame.size.width/2 + frame.size.height/2 * 0.8, y: frame.size.height/2)
layer.addSublayer(airplaneLayer)
airplaneLayer.opacity = 0.0
Copy the code

Step by step change the opacity of the plane layer when you drop it down and add in redrawFromProgress() :

airplaneLayer.opacity = Float(progress)
Copy the code

At the end of the stroke

Add to beginRefreshing() :

let strokeStartAnimation = CABasicAnimation(keyPath: "strokeStart")
strokeStartAnimation.fromValue = -0.5
strokeStartAnimation.toValue = 1.0

let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimation.fromValue = 0.0
strokeEndAnimation.toValue = 1.0
Copy the code

Add the following code to the end of beginRefreshing() to run both animations simultaneously:

let strokeAnimationGroup = CAAnimationGroup()
strokeAnimationGroup.duration = 1.5
strokeAnimationGroup.repeatDuration = 5.0
strokeAnimationGroup.animations = [strokeEndAnimation, strokeEndAnimation]
ovalShapeLayer.add(strokeAnimationGroup, forKey: nil)
Copy the code

In the code above, create an animation group and repeat the animation five times. This should be long enough to keep the animation running while the refresh view is visible. Then, add two animations to the group and add the group to the load layer.

Operation effect:

Create path keyframe animation

In layer 12- Keyframe Animation and Structure Properties learned to use the Values property to set keyframe animation. Let’s learn another way to use keyframe animation.

Add an airplane animation at the end of beginRefreshing() :

// Plane animation
let flightAnimation = CAKeyframeAnimation(keyPath: "position")
flightAnimation.path = ovalShapeLayer.path
flightAnimation.calculationMode = CAAnimationCalculationMode.paced

let flightAnimationGroup = CAAnimationGroup()
flightAnimationGroup.duration = 1.5
flightAnimationGroup.repeatDuration = 5.0
flightAnimationGroup.animations = [flightAnimation]
airplaneLayer.add(flightAnimationGroup, forKey: nil)
Copy the code

CAAnimationCalculationMode paced is another way to control the animation time, at this time will be at a constant speed core animation set animation, ignore any keyTimes Settings, this very useful for generated from any path smooth animation.

CAAnimationCalculationMode there are several other models, to view the official document in detail.

Operation effect:

This is a little strange. As ✈️ moves, the Angle changes accordingly.

Insert the following new animation code above the line that creates the flightAnimationGroup to adjust the Angle of the aircraft as it moves

let airplaneOrientationAnimation = CABasicAnimation(keyPath: "transform.rotation")
airplaneOrientationAnimation.fromValue = 0
airplaneOrientationAnimation.toValue = 2.0 * .pi
Copy the code

The final result

16- Copy the animation

Replicating Animations this chapter learns about Replicating Animations.

CAReplicatorLayer is another subclass of CALayer. It simply means that when something is created — it can be a shape, an image or anything else that can be drawn with layers — CAReplicatorLayer can copy it on the screen, as shown below:

Why copy shapes or images?

The super power of CAReplicatorLayer is to make each clone slightly different from the parent. For example, you can gradually change the color of each copy. The original layer may have been magenta, but on each copy, change the color to cyan:

Additionally, transformations can be applied between replicas. For example, a simple rotation transformation could be applied between each copy, drawing them as circles, as follows:

But the best feature is the ability to set the animation delay for each copy. When the instanceDelay of the original content is set to 0.2 seconds, the animation will be delayed 0.2 seconds in the first copy, 0.4 seconds in the second copy, 0.6 seconds in the third copy, and so on.

This can be used to create engaging and complex animations.

In this chapter, we will create an animation that mimics Siri and generates waves according to the voice after hearing it. The initial project is called Iris.

This project will create two different replicates. First, there’s the visual feedback animation that plays during the Iris session, which looks a lot like a psychedelic sine wave:

Then there’s an interactive microphone-driven audio wave that will provide visual feedback as the user speaks:

These two animations cover most of the CAReplicatorLayer.

Replicating like rabbits

Start Project Overview

Open the Main. Storyboard:

There is only one view controller, and it has a button and a label. Users ask questions when they press the button; When they release the button, Iris responds. The TAB is used to display microphone input and Iris’s answers.

In viewController.swift, the button event is connected to the action. ActionStartMonitoring () is triggered when the user touches the button; ActionEndMonitoring () is triggered when the user lifts a finger.

There are two additional classes that are beyond the scope of this chapter:

Assistant: Artificial intelligence Assistant. It has a predefined list of interesting answers and says them based on the user’s questions. MicMonitor: Monitors input on the iPhone microphone and repeatedly calls the closure expression you provide. This is where you have a chance to update the display.

Here we go!

Set up the replicator layer

Open viewController.swift and add the following two properties:

let replicator = CAReplicatorLayer(a)let dot = CALayer(a)Copy the code

Dot uses CALayer to draw basic simple shapes. The Replicator acts as a replicator for future dot copies.

Let’s add some constant attributes:

let dotLength: CGFloat = 6.0
let dotOffset: CGFloat = 8.0
Copy the code

DoLength is used as the width and height of the point layer, and dotOffset is the offset between the copies of each point.

Add the replicator layer to the view controller’s view, in viewDidLoad() add:

replicator.frame = view.bounds
view.layer.addSublayer(replicator)
Copy the code

The next step is to set up the dot layer. In viewDidLoad() add:

dot.frame = CGRect(x: replicator.frame.size.width - dotLength, y: replicator.position.y, width: dotLength, height: dotLength)
dot.backgroundColor = UIColor.lightGray.cgColor
dot.borderColor = UIColor(white: 1.0, alpha: 1.0).cgColor
dot.borderWidth = 0.5
dot.cornerRadius = 1.5

replicator.addSublayer(dot)
Copy the code

First position the dot layer on the right edge of the duplicator, then set the background color of the layer and add a border, etc. Finally, add the dot layer to the duplicator layer. Running results:

InstanceCount: Number of copies instanceTransform: conversion between copies instanceDelay: animation delay between copies

In viewDidLoad() add:

replicator.instanceCount = Int(view.frame.size.width / dotOffset)
replicator.instanceTransform = CATransform3DMakeTranslation(-dotOffset, 0.0.0.0)
Copy the code

Screen width divided by offset, set the number of copies for different screen widths. For example, a 5.5-inch instanceCount is 51 and a 4.7-inch instanceCount is 46…

Each copy moves 8 to the left (-dotoffset). The result is:

Test the copied animation

Add a little test animation to see what instanceDelay does. At the end of viewDidLoad() add:

let move = CABasicAnimation(keyPath: "position.y")
move.fromValue = dot.position.y
move.toValue = dot.position.y - 50.0
move.duration = 1.0
move.repeatCount = 10
dot.add(move, forKey: nil)
Copy the code

The animation is simple, just move the point up repeatedly 10 times.

At the end of the code above add:

replicator.instanceDelay = 0.02
Copy the code

Effect:

Before continuing, you need to delete the above test animation, except for instanceDelay.

Duplicate multiple animations

In this section, you will learn about animations that play when Iris speaks. To do this, you will combine several simple animations with different delays to produce the final effect.

Zoom animation

First, add the following animation in startSpeaking() :

let scale = CABasicAnimation(keyPath: "transform")
scale.fromValue = NSValue(caTransform3D: CATransform3DIdentity)
scale.toValue = NSValue(caTransform3D: CATransform3DMakeScale(1.4.15.1.0))
scale.duration = 0.33
scale.repeatCount = .infinity
scale.autoreverses = true
scale.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
dot.add(scale, forKey: "dotScale")
Copy the code

This is a simple layer animation that focuses on selecting a few parameters in a CATransform3DMakeScale. Here scale the dot layer 15 times vertically.

Run and click the gray button to call actionStartMonitoring, actionEndMonitoring(), and startSpeaking() respectively.

Try modifying a few parameters to the CATransform3DMakeScale and Duration to see what happens.

Transparent animation

Add a fade animation to startSpeaking() :

let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = 1.0
fade.toValue = 0.2
fade.duration = 0.33
fade.beginTime = CACurrentMediaTime() + 0.33
fade.repeatCount = .infinity
fade.autoreverses = true
fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
dot.add(fade, forKey: "dotOpacity")
Copy the code

The duration is the same as the zoom animation, but the delay is 0.33 seconds, the opacity is 1.0 to 0.2, and when the “wave” has moved sufficiently, the effect begins to fade out.

When two animations are running at the same time, it works a little better:

Color animation

Set the point background color change animation and add it to startSpeaking() :

let tint = CABasicAnimation(keyPath: "backgroundColor")
tint.fromValue = UIColor.magenta.cgColor
tint.toValue = UIColor.cyan.cgColor
tint.duration = 0.66
tint.beginTime = CACurrentMediaTime() + 0.28
tint.fillMode = kCAFillModeBackwards
tint.repeatCount = .infinity
tint.autoreverses = true
tint.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
dot.add(tint, forKey: "dotColor")
Copy the code

Three animation effects:

The properties of CAReplicatorLayer

We’ve done a lot of dazzling effects with the replicator layer. Since CAReplicatorLayer is itself a layer, you can animate some of its properties as well.

You can animate the CAReplicatorLayer’s basic properties (such as Position, backgroundColor, or cornerRadius), as well as cool animations with its special properties.

CAReplicatorLayer’s unique animatable properties include (three have already been described) :

InstanceDelay: animation delay between copies instanceTransform: conversion between copies instanceColor: Color instanceRedOffset, instanceGreenOffset, instanceBlueOffset: Apply increments to each instance of the color component instanceAlphaOffset: Transparency increments

Add an animation at the end of startSpeaking() :

let initialRotation = CABasicAnimation(keyPath: "instanceTransform.rotation")
initialRotation.fromValue = 0.0
initialRotation.toValue = 0.01
initialRotation.duration = 0.33
initialRotation.isRemovedOnCompletion = false
initialRotation.fillMode = kCAFillModeForwards
initialRotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
replicator.add(initialRotation, forKey: "initialRotation")     
Copy the code

It just has a tiny rotation on it, and the effect is:

For another twist up and down effect, add the following animation to complete the effect:

let rotation = CABasicAnimation(keyPath: "instanceTransform.rotation")
rotation.fromValue = 0.01
rotation.toValue   = -0.01
rotation.duration = 0.99
rotation.beginTime = CACurrentMediaTime() + 0.33
rotation.repeatCount = .infinity
rotation.autoreverses = true
rotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
replicator.add(rotation, forKey: "replicatorRotation")
Copy the code

. This is the instanceTransform rotation to run on the second animation, it in before the first animation is completed. Set the rotation from 0.01 radians (the final value of the first animation) to -0.01 radians, and you have the twist effect (rotation in different directions). Effect:

Let’s simulate the voice assistant and pretend to answer the order. At the beginning of startSpeaking() add:

meterLabel.text = assistant.randomAnswer() assistant.speak(meterLabel.text! , completion: endSpeaking) speakButton.isHidden =true
Copy the code

Get a random answer from the Assistant class, display it on meterLabel, read the answer, and then call the endSpeaking method. This is the process during which buttons need to be hidden.

After that, you need to delete all running animations and add the following in endSpeaking() :

replicator.removeAllAnimations()
Copy the code

Next, you need to set the dot layer “elegantly” to the original scale of the animation and in endSpeaking() continue add:

let scale = CABasicAnimation(keyPath: "transform")
scale.toValue = NSValue(caTransform3D: CATransform3DIdentity)
scale.duration = 0.33
scale.isRemovedOnCompletion = false
scale.fillMode = kCAFillModeForwards
dot.add(scale, forKey: nil)
Copy the code

The above animation, which does not specify fromValue, will start the animation from the current value and transform to CATransform3DIdentiy.

Finally, delete the remaining animations currently running in the DOT and restore the speak button state. In endSpeaking() continue add:

dot.removeAnimation(forKey: "dotColor")
dot.removeAnimation(forKey: "dotOpacity")
dot.backgroundColor = UIColor.lightGray.cgColor
speakButton.isHidden = false
Copy the code

Effects of this section:

Interactive copy animation

There will only be corresponding wave animation if Iris answers in the front. What we want to do in this section is to animate when the user holds down the button to speak (ask a question).

In actionStartMonitoring() add:

    dot.backgroundColor = UIColor.green.cgColor
    monitor.startMonitoringWithHandler { (level) in
        self.meterLabel.text = String(format: "%.2f db", level)
    }
Copy the code

ActionStartMonitoring is triggered when the user presses the speak button. To indicate “listening”, change the color of the dot layer to green.

And call on the monitor instance startMonitoringWithHandler (), its parameters is a closure piece, will be executed repeatedly, get the microphone decibels (db).

The decibel numbers on this side are a little different from the range we normally see, it’s in the range of -160.0 dB to 0.0 dB, -160.0 db is the quietest, 0.0 db means very loud.

Add the following code to the closure above:

    monitor.startMonitoringWithHandler { (level) in
        self.meterLabel.text = String(format: "%.2f db", level)
        let scaleFactor = max(0.2.CGFloat(level) + 50) / 2
    }
Copy the code

ScaleFactor stores values between 0.1 and 25.0.

Add a new property to ViewController:

var lastTransformScale: CGFloat = 0.0
Copy the code

For scaling animations where the scale is constantly changing, lastTransformScale holds the last scaling value.

Add the user’s voice animation to the microphone handling closure above:

let scale = CABasicAnimation(keyPath: "transform.scale.y")
scale.fromValue = self.lastTransformScale
scale.toValue = scaleFactor
scale.duration = 0.1
scale.isRemovedOnCompletion = false
scale.fillMode = kCAFillModeForwards
self.dot.add(scale, forKey: nil)
Copy the code

Finally, save the lastTransformScale and add the code above:

self.lastTransformScale = scaleFactor
Copy the code

You need to reset the animation and stop listening to the microphone when the user’s finger leaves the button. At the beginning of actionEndMonitoring() add:

monitor.stopMonitoring()
dot.removeAllAnimations()
Copy the code

At this point, the effect is:

Smooth the transition between microphone input and Iris animation

After careful consideration of the previous effect, I found that there was no transition between the user’s microphone input animation and Iris animation, which was directly skipped. This is the result of dot.removeallanimations () in actionEndMonitoring().

Replace dot.removeallanimations () with:

// Transition between microphone input and Iris animation
let scale = CABasicAnimation(keyPath: "transform.scale.y")
scale.fromValue = lastTransformScale
scale.toValue = 1.0
scale.duration = 0.2
scale.isRemovedOnCompletion = false
scale.fillMode = kCAFillModeForwards
dot.add(scale, forKey: nil)

dot.backgroundColor = UIColor.magenta.cgColor

let tint = CABasicAnimation(keyPath: "backgroundColor")
tint.fromValue = UIColor.green.cgColor
tint.toValue = UIColor.magenta.cgColor
tint.duration = 1.2
tint.fillMode = kCAFillModeBackwards
dot.add(tint, forKey: nil)
Copy the code

Final effect of this chapter:

This article is in my personal blog address: System learning iOS animation three: layer animation