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

UIViewPropertyAnimator was introduced in iOS10 to create easily interactive, interruptible and/or reversible view animations.

This class makes certain types of view animations easier to create and worth learning.

UIViewPropertyAnimator makes it easy to wrap many apis together in the same class, making it easier to use.

In addition, this new class does not completely replace UIView.animate(withDuration…). API set.

Content preview:

20 – UIViewPropertyAnimator primer

21 – UIViewPropertyAnimator deeply

22- Interactive animation with UIViewPropertyAnimator

23- Custom view Controller transitions with UIViewPropertyAnimator

All four sections of this article use the same project LockSearch

20 – UIViewPropertyAnimator primer

Prior to iOS10, the only option for creating view-based animations was uiview.animate (withDuration:…). I, but this set of apis does not provide the developer with a way to pause or stop an animation that is already running. In addition, developers can only use layer-based CAAnimation for reverse, speed up, or slow down animations.

UIViewPropertyAnimator is a class that allows animation to stay running, allows developers to adjust the currently running animation and provides detailed information about the current state of the animation.

Of course, simple single view animations use uiView.animate (withDuration:…) directly. That’s it.

Based on the animation

This chapter begins with the project LockSearch. This is similar to the iOS screen lock screen. The initial view controller has a search bar, a single widget, an edit button, and so on:

The start project has implemented some non-animation related features. For example, if you click the Show More button, the widget expands and shows More items. If I hit Edit, it’s going to go to another view controller, which is a simple TableView.

Of course, this project only simulates the lock screen in iOS for learning animations, and has no actual functionality.

Open the LockScreenViewController. Swift and to the view controller to add a new viewWillAppear (_) methods:

override func viewWillAppear(_ animated: Bool) {
    tableView.transform = CGAffineTransform(scaleX: 0.67, y: 0.67)
    tableView.alpha = 0
}
Copy the code

To create a simple zoom and fade-in view animation, first shrink the entire table view and make it transparent.

Next, create an animator when the view controller’s view appears on the screen. Add the following to the LockScreenViewController:

override func viewDidAppear(_ animated: Bool) {
    let scale = UIViewPropertyAnimator.init(duration: 0.33, curve: .easeIn) {
    }
}
Copy the code

Here, you use UIViewPropertyAnimator a convenient constructor: UIViewPropertyAnimator. Init (duration: the curve: animations:).

The constructor creates an animation instance and sets the total duration and time curve of the animation. The type of the latter parameter is UIViewAnimationCurve, which is an enumerated type with four types: easeInOut, easeIn, easeOut, and Linear. The UIView. The animate (withDuration:…). Options are similar in.

Add animation

Add to viewDidAppear(_:) :

scale.addAnimations {
    self.tableView.alpha = 1.0
}
Copy the code

Add animate code blocks with addAnimations, like UIView.animate(withDuration…) The closure parameter animations. The difference with animators is that you can add multiple animation blocks.

In addition to being able to conditionally build complex animations, you can also add animations with varying delays. Another version of addAnimations takes two arguments: the animation code delayFactor the delay before the animation begins

DelayFactor and UIView. The animate (withDuration…). In, it is between 0.0 and 1.0, not absolute but relative time.

Add a second animation in the same animator, but with some delay. Continue after the above code and add:

scale.addAnimations({
    self.tableView.transform = .identity
}, delayFactor: 0.33)
Copy the code

The actual delay time is delayFactor times the remaining duration of the animator. The animation has not yet been started, so the remaining duration is equal to the total duration. So in the case above:

delayFactor(0.33) * remainingDuration(=duration 0.33) = delay of 0.11 seconds
Copy the code

Why is the second argument not a simple value in seconds? Imagine that the animator is already running and you decide to add some new animations halfway through. In this case, the remaining duration will not equal the total duration because some time has passed since the animation was started.

In this case, delayFactor will allow developers to set delay animations based on the time remaining. In addition, this design ensures that the delay cannot be set to longer than the remaining running time.

Add a completion closure

Add to viewDidAppear(_:) :

scale.addCompletion { (_) in
    print("ready")}Copy the code

AddCompletion (_:) is the animation completion closure, which can also be called multiple times to complete multiple handlers.

To start the animation, add at the end of viewWillAppear(_:) :

scale.startAnimation()
Copy the code

Extraction of animation

For code clarity, you can put the animation code together in a class.

Create a new file named animatorFactory.swift and replace its default content with:

import UIKit

class AnimatorFactory {}Copy the code

Then add a type method that contains the animation code you just wrote, but does not run the animation by default, instead returning to the animator:

static func scaleUp(view: UIView) -> UIViewPropertyAnimator {
    let scale = UIViewPropertyAnimator(duration: 0.33, curve: .easeIn)

    scale.addAnimations {
        view.alpha = 1.0
    }

    scale.addAnimations({
        view.transform = .identity
    }, delayFactor: 0.33)

    scale.addCompletion { (_) in
                         print("ready")}return scale
}
Copy the code

This method takes the view as an argument, creates all animations on that view, and finally returns the prepared animator.

Replace viewDidAppear(_:) in LockScreenViewController with:

override func viewDidAppear(_ animated: Bool) {
    AnimatorFactory.scaleUp(view: tableView).startAnimation()
}
Copy the code

So it looks a little bit cleaner and cleaner, moving the animation code out of the view controller.

The AnimatorFactory 🏭 class focuses on animation code, which is a simple application of the Factory pattern in design mode. πŸ˜€

Run animator

The search bar fades into the blurView when the user is using it and fades out when the user is finished searching.

Add a new method to the LockScreenViewController class:

func toggleBlur(_ blurred: Bool) {
    UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.5, delay: 0.1, options: .curveEaseOut, animations: {
        self.blurView.alpha = blurred ? 1 : 0
    }, completion: nil)}Copy the code

UIViewPropertyAnimator.runningPropertyAnimator(withDuration:…) With the UIView. The animate (withDuration:…). It has exactly the same parameters and uses exactly the same.

While this may seem like a “fire-and-forget” API, note that it does return an instance of animation. As a result, you can add more animations, more completion blocks, and often interact with the animation that is currently running.

Now let’s look at the fade-in animation. The LockScreenViewController is set up as a delegate for the search bar, so you just need to implement the required methods to trigger the animation at the right time.

Follow the search bar proxy protocol for LockScreenViewController in an extended manner:

extension LockScreenViewController: UISearchBarDelegate {
  
  func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
    toggleBlur(true)}func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
    toggleBlur(false)}}Copy the code

To provide users with the ability to cancel a search, add the following two methods:

  func searchBarResultsListButtonClicked(_ searchBar: UISearchBar) {
    searchBar.resignFirstResponder()
  }
  
  func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    if searchText.isEmpty{
      searchBar.resignFirstResponder()
    }
  }
Copy the code

This will allow users to unsearch by clicking a button on the right.

Operation, effect:

Click on the text field of the search bar and the widget disappears in the blur view. When you click the button to the right of the search bar, the blurred view fades out.

Basic keyframe animation

UIViewPropertyAnimator can also use UIView.addKeyFrame (5- view keyframe animation). Let’s create a simple icon shake animation to show it.

Add a type method to AnimatorFactory:

  static func jiggle(view: UIView) -> UIViewPropertyAnimator {
    return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.33, delay: 0
      , animations: {
        UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations: {
          view.transform = CGAffineTransform(rotationAngle: -.pi/8)})UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.75, animations: {
          view.transform = CGAffineTransform(rotationAngle: +.pi/8)})UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 1.0, animations: {
          view.transform = CGAffineTransform.identity
        })
    }, completion: { (_) in})}Copy the code

The first keyframe rotates to the left, the second keyframe rotates to the right, and finally the third keyframe returns to the origin.

To ensure that the icon remains in its original position, add the following to the completion closure:

view.transform = .identity
Copy the code

Now you can add an animation to the view where you want to run the animation.

Open iconcell. swift (the file is in the Widget subfolder). This is the custom unit class that corresponds to each icon in the widget view. Add to IconCell:

func iconJiggle(a) {
    AnimatorFactory.jiggle(view: icon)
}
Copy the code

Now Xcode complains that the animatorFactory. jiggle method returns a result that is not being used, which is a friendly reminder from Xcode 😊.

This problem can be easily solved by adding @discardableresult before the jiggle method to let Xcode know the result of this method I don’t want 😏.

DiscardableResult official explanation:

Apply this attribute to a function or method declaration to suppress the compiler warning when the function or method that returns a value is called without using its result.

  @discardableResult
  static func jiggle(view: UIView) -> UIViewPropertyAnimator {
Copy the code

To finally run the animation, add to the collectionView(_:didSelectItemAt:) of WidgetView.swift:

if let cell = collectionView.cellForItem(at: indexPath) as? IconCell {
    cell.iconJiggle()
}
Copy the code

Effect:

Extract blur animation

Extract the previous blur animation into the AnimatorFactory as well.

@discardableResult
static func fade(view: UIView, visible: Bool) -> UIViewPropertyAnimator {
    return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.5, delay: 0.1, options: .curveEaseOut, animations: {
        view.alpha = visible ? 1.0 : 0.0
    }, completion: nil)}Copy the code

Instead of the toggleBlur (_:) method in LockScreenViewController:

func toggleBlur(_ blurred: Bool) {
    AnimatorFactory.fade(view: blurView, visible: blurred)
}
Copy the code

Preventing animation overlap

How do I check if the animator is currently executing his animation?

If you click on the same icon in quick succession, you will see that the dither animation starts again without ending.

To solve this problem, you need to check whether the view has an animation running.

Add an attribute to IconCell and modify iconJiggle() :

  var animator: UIViewPropertyAnimator?

  func iconJiggle(a) {
    if let animator = animator, animator.isRunning {
      return
    }

    animator = AnimatorFactory.jiggle(view: icon)
  }
Copy the code

Comparison shows the difference:

21 – UIViewPropertyAnimator deeply

After learning the basics of UIViewPropertyAnimator in the previous section, this section will learn more about UIViewPropertyAnimator.

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

Custom animation timing

It has been mentioned many times before: easeInOut, easeIn, easeOut and Linear (which can be understood as the curve type of the object’s movement track). You can look at animation easing in view animation or animation easing in layer animation, I’m not going to talk about it here.

Built-in time curve

Currently, when you activate the search bar, you fade in and out of the blurred view at the top of the widget. In this example, you will remove the fade-in animation and animate the blur effect itself.

Previously, when the search bar was activated, there was a fade in and out effect in the blurred view. This section removes the effect and modifies the Settings to animate the blur effect itself. What does that mean? So if you look at the following operation, it should make sense.

Add a new method to the LockScreenViewController class:

func blurAnimations(_ blured: Bool)- > () - >Void {
    return {   
      self.blurView.effect = blured ? UIBlurEffect(style: .dark) : nil
    self.tableView.transform = blured ? CGAffineTransform(scaleX: 0.75, y: 0.75) : .identity
    self.tableView.alpha = blured ? 0.33 : 1.0}}Copy the code

Delete two lines of code in viewDidLoad() :

    blurView.effect = UIBlurEffect(style: .dark)
    blurView.alpha = 0
Copy the code

Instead of toggleBlur(_:)

func toggleBlur(_ blurred: Bool) {
    UIViewPropertyAnimator(duration: 0.55, curve: .easeOut, animations: blurAnimations(blurred)).startAnimation()
}
Copy the code

Operation, effect:

Note that blurring doesn’t just fade in or out, it actually inserts blurring into the effect view.

Bessel curve

Sometimes when you want to be very specific about the time of the animation, using these curves to simply “start slowing down” or “slow down” is not enough.

Learn how to use the CAMediaTimingFunction to control the timing of layer animations in 10- Animation Group and Time Controls.

I didn’t know what was going on behind it the Bezier curve, but let me introduce it here. This can also be applied to layer animations.

What is the Bezier curve?

Let’s start with something simple — a line. It is very neat and requires drawing A line on the screen, just defining the coordinates of its two points, starting (A) and ending (B) :

Now let’s look at the curve. Curves are more interesting than lines because they can draw anything on the screen. Such as:

What you see above is four curves coming together; Their ends meet at the little white square. What’s interesting in this picture are the little green circles, which define each curve.

So the curve is not random. They also have some details, like lines, that help us define them by coordinates.

You can define curves by adding control points to lines. Let’s add a control point to the previous line:

Imagine a curve drawn by a pencil connected to a line, with its beginning moving along line AC and its end moving along line CB:

I found a GIF online:

Bezier curves with one control point are called conic curves. Bezier curves with two control points are called cubic Bezier curves. The built-in curve we use is the cubic curve.

The core animation uses a cubic curve that always begins at the coordinate (0,0), which indicates the beginning of the animation duration. Of course, these time curves always end at (1,1), indicating the duration of the animation and the end of the progression.

Let’s look at the ease-in curve:

Over time (moving horizontally from left to right in coordinate space), the curve progresses very little on the vertical axis, and then after about half the duration of the animation, the curve progresses very much on the vertical axis, ending at (1, 1).

Ease-out and ease-in-out curves are:

Now that you know how Bezier curves work, it’s just a matter of visually designing some curves and getting the coordinates of the control points so you can use them for iOS animations.

You can use cubic-bezier.com. This is the very handy site of computer science researcher and speaker Lea Verou. It can drag two control points of the cubic Bezier and view real-time animation previews, very nice😊😊.

Bessel’s principle above is not profound enough πŸ€¦β™€οΈ, now only need to understand the curve, through two control points can draw the curve.

Next, add custom timing animations to the project.

Replace the existing animation of toggleBlur() in LockScreenViewController with:

func toggleBlur(_ blurred: Bool) {
    UIViewPropertyAnimator(duration: 0.55, controlPoint1: CGPoint(x: 0.57, y: -0.4), controlPoint2: CGPoint(x: 0.96, y: 0.87), animations: blurAnimations(blurred)).startAnimation()
}
Copy the code

The controlPoint1 and controlPoint2 points over here are the control points for our custom cubic curve.

Control points can be selected via cubic-bezier.com.

Spring animation

Another convenient constructor UIViewPropertyAnimator (duration: dampingRatio: animations:), is used to define the spring animation.

This is the same as uiView.animate (withDuration: Delay: usingSpringWithDamping: initialSpringVelocity: options: animations: Completion: : Similar, except that the initial speed is 0.

Custom time curve

UIViewPropertyAnimator class has a constructor UIViewPropertyAnimator (duration: timingParameters:).

The timingParameters parameter must comply with the UITimingCurveProvider protocol. There are two classes available: UICubicTimingParameters and UISpringTimingParameters.

Let’s look at how this constructor is used.

Damping and velocity

Add damping and speed as follows:

let spring = UISpringTimingParameters(dampingRatio:0.5, initialVelocity: CGVector(dx: 1.0, dy: 0.2))

let animator = UIViewPropertyAnimator(duration: 1.0, timingParameters: spring)
Copy the code

Note that initialVelocity is a vector type and this parameter is optional.

Custom spring animation

If you want to spring the animation more specific Settings, can be another constructor UISpringTimingParameters init (mass, stiffness, damping: initialVelocity:), the code is as follows:

let spring = UISpringTimingParameters(mass: 10.0, stiffness: 5.0, damping: 30, initialVelocity: CGVector(dx: 1.0, dy: 0.2))

let animator = UIViewPropertyAnimator(duration: 1.0, timingParameters: spring) 
Copy the code

See the previous article 11- Layer spring animation for how these parameters work.

Automatic layout animation

The previous article systematically studied iOS animation part two: Automatic layout animation.

Use UIViewPropertyAnimator to constrain layout animation with UIView.animate(withDuration:…) The way they are created is very similar. The trick is to update the constraint by calling layoutIfNeeded() in the animation block.

Add a new factory method to AnimatorFactory:

@discardableResult
static func animateConstraint(view: UIView, constraint: NSLayoutConstraint, by: CGFloat) -> UIViewPropertyAnimator {
    let spring = UISpringTimingParameters(dampingRatio: 0.55)
    let animator = UIViewPropertyAnimator(duration: 1.0, timingParameters: spring)

    animator.addAnimations {
        constraint.constant += by
        view.layoutIfNeeded()
    }
    return animator
}
Copy the code

In the LockScreenViewController viewWillAppear add:

dateTopConstraint.constant -= 100
view.layoutIfNeeded()
Copy the code

In viewDidAppear add:

AnimatorFactory.animateConstraint(view: view, constraint: dateTopConstraint, by: 150).startAnimation()
Copy the code

This allows the location of the time TAB to have an animation when the app opens.

Next, add a constraint animation. When “Show more” is clicked, the widget loads content and needs to change its height constraints.

Redefine the toggleShowMore(_:) method in WidgetCell.swift:

@IBAction func toggleShowMore(_ sender: UIButton) {
    self.showsMore = !self.showsMore

    let animations = {
        self.widgetHeight.constant = self.showsMore ? 230 : 130
        if let tableView = self.tableView {
            tableView.beginUpdates()
            tableView.endUpdates()
            tableView.layoutIfNeeded()
        }
    }
    let spring = UISpringTimingParameters(mass: 30, stiffness: 10, damping: 300, initialVelocity: CGVector(dx: 5, dy: 0))

    toggleHeightAnimator = UIViewPropertyAnimator(duration: 0.0, timingParameters: spring) toggleHeightAnimator? .addAnimations(animations) toggleHeightAnimator? .startAnimation() }Copy the code

At the bottom of the toggleShowMore(_:) method, add the following code to load the icon in the widget:

widgetView.expanded = showsMore
widgetView.reload()
Copy the code

View the transition

In view animation’s 3-transition animation, you learned about view transitions. Now I’m going to use UIViewPropertyAnimator to do the view transition.

Title shows More buttons, “Show More” and “Show Less” both fade in and out of each other.

Add this code before the toggleShowMore (_ πŸ™‚ toggleHeightAnimator definition:

let textTransition = {
    UIView.transition(with: sender, duration: 0.25, options: .transitionCrossDissolve, animations: {
        sender.setTitle(self.showsMore ? "Show Less" : "Show More".for: .normal)
    }, completion: nil)}Copy the code

Add before toggleHeightAnimator starts:

toggleHeightAnimator? .addAnimations(textTransition, delayFactor:0.5)
Copy the code

This will change the button title, with a nice crossover fade-in effect:

Effects can also be tried. TransitionFlipFromTop, etc

22- Interactive animation with UIViewPropertyAnimator

The previous two chapters covered many uses of UIViewPropertyAnimators, such as basic animations, custom timing and spring animations, and animation extraction. However, in contrast to the previous “fire-and-forget” API for view animation, we haven’t explored what makes UIViewPropertyAnimator really interesting.

UIView.animate(withDuration:…) Provides a method for setting the animation, but once the animation end state is defined, the animation will start execution without control.

But what if we want to interact with the animation while it’s running? In particular, animations are not static, but driven by user gestures or microphone input, just as we learned in iOS Animation 3: Layer Animation in the previous layer animation system.

Animations created using UIViewPropertyAnimator are fully interactive: you can start, pause, change speed, and even adjust progress directly.

Since UIViewPropertyAnimator can drive both preset and interactive animations, it is a bit more complicated to describe the animator’s current state 😡. Here’s how to handle animator state.

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

Animation state machine

UIViewPropertyAnimator can check whether the animation has been started (isRunning), paused or completely stopped (state), or whether the animation has been reversed (isReversed).

UIViewPropertyAnimator has three properties that describe the current state:

IsRunning (read only) : Whether the animation is currently in motion. It defaults to false, becomes true when startAnimation() is called, and will change to false again if the animation is paused or stopped, or if it completes naturally.

IsReversed: The default is false because we always start the animation forward, that is, the animation is played from the start state to the end state. If changed to true, the animation will be reversed, from the introduction state to the start state.

State (read only) :

State defaults to Inactive, which usually means that the animator has just been created and no methods have been called. Note that this is different from setting isRunning to false, isRunning actually only focuses on the animation in progress, whereas when state is inactive, it actually means the animator hasn’t done anything yet.

State becomes active when:

  • callstartAnimation()To start the animation
  • Called when the animation has not startedpauseAnimation()
  • Set up thefractionCompleteProperty to “rewind” the animation to a location

When the animation completes naturally, state switches back to.inactive.

If stopAnimation() is called on the animator, it sets its state property to.stopped. In this state, the only thing you can do is either abandon the animator completely or call finishAnimation(at:) to complete the animation and send the animator back to.inactive.

As you might expect, UIViewPropertyAnimator can only switch between states in a specific order. It cannot go directly from Inactive to stopped, nor can it go directly from STOPPED to Active.

If you set up pausesOnCompletion, instead of automatically stopping the animation once the animator has finished running it, you pause it. This will give us the opportunity to continue using it in a suspended state.

Status flow chart:

It might be a little convoluted, but if you have any questions, you can go back to this section.

Interactive 3D Touch animation

Starting with this section, you will learn to create interactive animations similar to 3D Touch interactions:

Note: For this chapter project, you need a 3D Touch compatible iOS device (6S+ if MEMORY serves).

I heard that πŸ‘‚, 3D Touch technology will be cancelled on iPhone, well, here is learning similar to 3D Touch animation, its future, I don’t know.

3D Touch animation can be described as follows: when we press an icon on the screen with our finger, the animation begins to interact, the background becomes more and more blurred, and a menu gradually appears from the icon. This process will change back and forth with the strength of the finger press.

The effect of slowing down is:

WidgetView. Swift, WidgetView by extending the observe UIPreviewInteractionDelegate agreement. This protocol includes some delegate methods from the 3D Touch process.

In order to let you start developing the movie itself, UIPreviewInteractionDelegate method has been connected to LockScreenViewController call related method. The code in WidgetView is as follows:

  • Called when 3D Touch startsLockScreenViewController.startPreview(for:).
  • When the user presses in the process, it may be harder (or softer) when repeatedly calledLockScreenViewController.updatePreview(percent:).
  • Called when the PEEK interaction completes successfullyLockScreenViewController.finishPreview().
  • Finally, if the user raises his finger without completing the preview gestureLockScreenViewController.cancelPreview().

Add these three properties to the LockScreenViewController that you need to create peep interactions:

var startFrame: CGRect?
var previewView: UIView?
var previewAnimator: UIViewPropertyAnimator?
Copy the code

StartFrame to track the start of the animation.

PreviewView Snapshot view of the icon that is temporarily used during animation. PreviewAnimator will be the interactive animator that drives preview animations.

Add another property to keep the blur effect to show the icon box:

let previewEffectView = IconEffectView(blur: .extraLight)
Copy the code

IconEffectView is a custom UIVisualEffectView subclass that contains a simple fuzzy view of a single label, used to simulate a pop-up menu from a pressed icon:

In an extension to LockScreenViewController that complies with WidgetsOwnerProtocol, implement the startPreview(for:) method:

func startPreview(for forView: UIView){ previewView? .removeFromSuperview() previewView = forView.snapshotView(afterScreenUpdates:false) view.insertSubview(previewView! , aboveSubview: blurView) }Copy the code

The WidgetsOwnerProtocol protocol is a custom protocol.

As soon as the user starts pressing the icon, WidgetView calls startPreview(for:). The for parameter is the set cell image where the user starts the gesture.

First remove any existing previewView views, just in case the previous view is left on the screen. You can then create a snapshot of the collection view icon and finally add it to the screen above the Blur Effect view.

Run, press the icon. The icon appears in the top left corner! 😰

Because its position has not been set. Continue to add:

previewView? .frame = forView.convert(forView.bounds, to: view) startFrame = previewView?.frame addEffectView(below: previewView!)Copy the code

The icon copy is now in the correct position and completely overlays the original icon. StartFrame stores the startFrame for later use.

Function addEffectView(below:) adds a blur box below the icon snapshot. The code is:

func addEffectView(below forView: UIView) {
    previewEffectView.removeFromSuperview()
    previewEffectView.frame = forView.frame

    forView.superview?.insertSubview(previewEffectView, belowSubview: forView)
}
Copy the code

Now create the animation itself and add the class method to the AnimatorFactory:

static func grow(view: UIVisualEffectView, blurView: UIVisualEffectView) -> UIViewPropertyAnimator {

    view.contentView.alpha = 0
    view.transform = .identity

    let animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeIn)

    return animator
}
Copy the code

Two parameters: View is the animated view and blurView is the blurred background of the animation.

Add animation and complete closure for animator before returning to animator:

animator.addAnimations {
    blurView.effect = UIBlurEffect(style: .dark)
    view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}

animator.addCompletion { (_) in
    blurView.effect = UIBlurEffect(style: .dark)
}
Copy the code

The animation code creates a blur transition for the blurView and a normal transition for the View.

After the LockScreenViewController. Swift startPreview () to complete the call:

previewAnimator = AnimatorFactory.grow(view: previewEffectView, blurView: blurView)
Copy the code

UpdatePreview (percent:)

func updatePreview(percent: CGFloat){ previewAnimator? .fractionComplete =max(0.01.min(0.99, percent))
}
Copy the code

The above method is called repeatedly when WidgetView is pressed. FractionComplete is in the range of 0.01 and 0.99, because I don’t want the animation to end at this point, I specify another way to complete or cancel the animation.

Run, effect (slow down) :

You will (surprise!) We need more animators. Open animatorFactory.swift and add an animator, which undoes everything your “grow” animator does. One case where you need this animator is when the user cancels the gesture. When you need to clean up the UI, the other is the final stage of a successful interaction.

Add a method to AnimatorFactory:

static func reset(frame: CGRect, view: UIVisualEffectView, blurView: UIVisualEffectView) -> UIViewPropertyAnimator {

    return UIViewPropertyAnimator(duration: 0.5, dampingRatio: 0.7, animations: {
        view.transform = .identity
        view.frame = frame
        view.contentView.alpha = 0

        blurView.effect = nil})}Copy the code

The three arguments to this method are the start frame of the original animation, the animation view, and the background blur view. The animation block resets all properties in the state before the interaction begins.

In LockScreenViewController. Swift, another method to realize WidgetsOwnerProtocol agreement:

func cancelPreview(a) {
    if let previewAnimator = previewAnimator {
        previewAnimator.isReversed = true
        previewAnimator.startAnimation()
    }
}
Copy the code

CancelPreview () is the method called when the WidgetView is pressed and suddenly raises its finger, cancelling the gesture in progress.

So far, you haven’t started your animator yet. You are repeatedly setting fractionComplete, which drives the animation interactively. However, once the user cancels the interaction, you cannot continue to drive the animation interactively because you have no more input. Instead, the animation can be played back to its initial state by setting isReversed to true and calling startAnimation(). Now this is uiView.animate (withDuration:…) Impossible to do!

Try one more interaction. Press half of the animation, then begin testing cancelPreview().

The animation plays correctly when you lift your finger, but eventually the dark blur suddenly reappears.

This problem is rooted in your growth animator code. Switch back to AnimatorFactory.swift and look at the code in grow (View: UIVisualEffectView, blurView: UIVisualEffectView) – more specifically, this part:

animator.addCompletion { (_) in
  blurView.effect = UIBlurEffect(style: .dark)
}
Copy the code

Animations can be played forward or backward and need to be handled in the completion closure.

The closure of addCompletion(), whose arguments are omitted with _, is actually an enumerated UIViewAnimatingPosition that represents what is currently happening with the animation. It can have three values:.start,.end, or.current.

Replace the completion closure with:

animator.addCompletion { (position) in
  switch position {
      case .start:
      blurView.effect = nil
      case .end:
      blurView.effect = UIBlurEffect(style: .dark)
      default:
      break}}Copy the code

If the animation is returned, remove the blur effect. If successful, the effect is explicitly adjusted to a dark blur effect.

Now there’s a new problem. If you unpress an icon, you can’t press it again! This is because the icon snapshot is still above the original icon, blocking the pinch gesture. To resolve this issue, the value needs to delete the snapshot immediately after the reset animation is complete.

In LockScreenViewController. Swift cancelPreview () goes on to add:

previewAnimator.addCompletion { (position) in
  switch position {
  case .start:
    self.previewView? .removeFromSuperview()self.previewEffectView.removeFromSuperview()
  default:
    break}}Copy the code

Note: addCompletion(_:) can be called multiple times without being replaced by the next one.

Let’s add another animator to display the icon menu. Switch to AnimatorFactory.swift and add to it:

static func complete(view: UIVisualEffectView) -> UIViewPropertyAnimator {

  return UIViewPropertyAnimator(duration: 0.3, dampingRatio: 0.7, animations: {
    view.contentView.alpha = 1
    view.transform = .identity
    view.frame = CGRect(x: view.frame.minX - view.frame.minX/2.5,
                        y: view.frame.maxY - 140,
                        width: view.frame.width + 120,
                        height: 60)})}Copy the code

This time you have created a simple spring animator. For animators, you can do the following:

  • Fade in the Custom Actions menu item.
  • Reset the transformation.
  • Set the view frame directly above the icon.

The location of the menu changes according to the icon the user presses.

You set the horizontal position to view.frame.minx-view.frame.minx /2.5 and display the right menu if the icon is on the left side of the screen, and the left menu on the right side of the screen if the icon is on the left. Please refer to the following differences:

Animators are ready, so open LockScreenViewController. Swift and finally a required is added in the WidgetsOwnerProtocol extension methods:

func finishPreview(a){ previewAnimator? .stopAnimation(false) previewAnimator? .finishAnimation(at: .end) previewAnimator =nil
}
Copy the code

FinishPreview () is called when the user presses the 3D touch gesture when you feel haptic feedback.

StopAnimation (_:) stops the animation currently running on the screen. The parameter is false and the animator status is stopped; The argument is true, the animator is inactive and clears all animations, and the completion closure is not called.

Once you put the animator on stop, you have a few options. What you’re after in finishPreview () is to tell the animator to complete its final state. Therefore, you call finishAnimation (at:.end); This updates all views with the target values of the plan animation and calls your finish.

This gesture no longer requires the previewAnimator, so you can remove it.

You can call finishAnimation (at πŸ™‚ using one of the following methods:

Start: Resets the animation to its initial state. Current: Updates the view’s properties from the current progress of the animation and completes.

After calling finishAnimation(at:), your animator is inactive.

Back to the Widgets project. Since you get rid of the preview animator, you can run the full animator to display the menu. Attach the following to the end of finishPreview () :

AnimatorFactory.complete(view: previewEffectView).startAnimation()
Copy the code

Run, press the icon:

Turn off the fuzzy view

Now, when the menu pops up and the blurred view is displayed, there is no action to go back to the original view. Let’s add this action.

Add the following code in finishPreview() to prepare for interactive blurring:

blurView.effect = UIBlurEffect(style: .dark)
blurView.isUserInteractionEnabled = true
blurView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismissMenu)))
Copy the code

Make sure that the blur effect is set to.dark, and then enable user interaction on the blur view itself. Add a click gesture without the blur view, allowing the user to click anywhere around the icon to close the menu.

The dismissMenu() code is:

@objc func dismissMenu(a) {
    let reset = AnimatorFactory.reset(frame: startFrame! , view: previewEffectView, blurView: blurView) reset.addCompletion { (_) in
                         self.previewEffectView.removeFromSuperview()
                         self.previewView? .removeFromSuperview()self.blurView.isUserInteractionEnabled = false
                        }
    reset.startAnimation()
}
Copy the code

Interactive keyframe animation

In the 20-UIViewPropertyAnimator tutorial, you learned how to make keyframe animations with UIViewPropertyAnimator. Now add interactive actions to keyframe animations.

To give it a try, you’ll add an extra element to the growing animation – an element that is interactively scrubbed when the user presses the icon.

Delete the code in the AnimatorFactory grow() method:

animator.addAnimations {
  blurView.effect = UIBlurEffect(style: .dark)
  view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)}Copy the code

Replace with:

animator.addAnimations {
    UIView.animateKeyframes(withDuration: 0.5, delay: 0.0, animations: {

        UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1.0, animations: {
            blurView.effect = UIBlurEffect(style: .dark)
            view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)})UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {
            view.transform = view.transform.rotated(by: -.pi/8)})})}Copy the code

The first keyframe runs the same animation you did before. The second keyframe is a simple rotation that looks like this:

23- Custom view Controller transitions with UIViewPropertyAnimator

You learned how to create custom view controller transitions in System Learning iOS Animation 4: Transition Animations for View Controllers. Learn how to customize view controller transitions using UIViewPropertyAnimator.

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

Static view controller transitions

Now, when you click the ** “Edit” ** button, the experience is very bad 😰.

Start by creating a new file, PresentTransition.swift, which, as the name suggests, is used for transitions. Replace its default content with:

import UIKit

class PresentTransition: NSObject.UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.75
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning){}}Copy the code

UIViewControllerAnimatedTransitioning agreement has been studied in system iOS 4: animation view controller of secondary schools in animated transitions.

I will create a transition animation where the original view gradually blurs out and the new view slowly moves out.

Add a new method to PresentTransition:

func transitionAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
    let duration = transitionDuration(using: transitionContext)
    
    let container = transitionContext.containerView
    let to = transitionContext.view(forKey: .to)!
    
    container.addSubview(to)
}
Copy the code

In the code above, you did the necessary preparatory work for the view controller transition. First get the animation duration, then get the view of the target view controller, and finally add this view to the transition container.

Next, you can set up the animation and run it. Add the following code to the transitionAnimator(using:) method above:

to.transform = CGAffineTransform(scaleX: 1.33, y: 1.33).concatenating(CGAffineTransform(translationX: 0.0, y: 200))
to.alpha = 0
Copy the code

This will stretch up, then move the view of the target view controller down, and finally fade it out.

Add an animator after to.alpha = 0 to run the transformation:

let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut)

animator.addAnimations({
    to.transform = CGAffineTransform(translationX: 0.0, y: 100)
}, delayFactor: 0.15)

animator.addAnimations({
    to.alpha = 1.0
}, delayFactor: 0.5)
Copy the code

There are two animations in the animator: moving the view of the target view controller to its final position and fading in.

Finally add the completion closure:

animator.addCompletion { (_) intransitionContext.completeTransition(! transitionContext.transitionWasCancelled) }return animator

Copy the code

Call the above transitionAnimator method in animateTransition(using:) :

transitionAnimator(using: transitionContext).startAnimation()
Copy the code

Define constant properties in LockScreenViewController:

let presentTransition = PresentTransition(a)Copy the code

Let LockScreenViewController observe UIViewControllerTransitioningDelegate agreement:

// MARK: - UIViewControllerTransitioningDelegate
extension LockScreenViewController: UIViewControllerTransitioningDelegate {
  
  func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return presentTransition
  }
}
Copy the code

UIViewControllerTransitioningDelegate agreement in iOS system learning animation four: view controller studied in animated transitions.

AnimationController (forPresented: presents: source:) method is to tell UIKit, I think the custom view controller transitions.

In LockScreenViewController, find ActionpresentSettings(_:) on the Edit button and add code:

settingsController = storyboard? .instantiateViewController(withIdentifier:"SettingsViewController") as! SettingsViewController
settingsController.transitioningDelegate = self
present(settingsController, animated: true, completion: nil)
Copy the code

Run, click the Edit button, SettingsViewController has a problem:

Change the view background to Clear Color in main.storyboard.

Run, become:

Add a new property for the animator below. In order to inject any custom animation into the transition animation, use the same transition class to generate a slightly different animation.

Add two new properties in PresentTransition:

var auxAnimations: (() -> Void)?
var auxAnimationsCancel: (() -> Void)?
Copy the code

In the transitionAnimator(using:) method before the animator returns:

if let auxAnimations = auxAnimations {
    animator.addAnimations(auxAnimations)
}
Copy the code

This allows you to add custom animations to the transformation depending on the situation. For example, add a blur animation for the current transition.

Open LockScreenViewController and insert at the beginning of presentSettings() :

presentTransition.auxAnimations = blurAnimations(true)
Copy the code

Try the transition again and see how this line changes it:

The blur animation was reused.

In addition, the fuzzy view needs to be hidden when the user disarms the controller.

Before present(_: Animated: Completion πŸ™‚ in presentSettings(_: πŸ™‚ add:

    settingsController.didDismiss = { [unowned self] in
      self.toggleBlur(false)}Copy the code

Now, run, click Cancel or another option in the SettingsViewController view, blur the view first, and then revert to the first view controller:

Interactive view controller transitions

This section learns to implement interactive view controller transitions by pulling down gestures.

First of all, let us use powerful UIPercentDrivenInteractionTransition class to enable view controller transitions of interactivity.

Open PresentTransition. Swift put the following:

class PresentTransition: NSObject.UIViewControllerAnimatedTransitioning
Copy the code

Replace with:

class PresentTransition: UIPercentDrivenInteractiveTransition.UIViewControllerAnimatedTransitioning {
Copy the code

UIPercentDrivenInteractiveTransition is a defined transitions method based on the “percentage” class, for example, there are three methods:

  • update(_:)Reverse transition.
  • cancel()Cancel the view controller transition.
  • finish()Play the transition until complete.

This is also covered in 19- Interactive Navigation Controller transitions.

Some properties of the UIPercentDrivenInteractiveTransition:

  • TimingCurve: you can set this property to provide a custom timingCurve for the animation if the transition is driven interactively and is played until the end.

  • WantsInteractiveStart: The default is true and whether to use interactive transitions.

  • Pause () : Call this method to pause a non-interactive transition and switch to interactive mode.

Add a new method to PresentTransition:

  func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
    return transitionAnimator(using: transitionContext)
  }
Copy the code

This is a way to UIViewControllerAnimatedTransitioning agreement. It allows us UIKit to provide interruptible animators.

The transition animator class now has two different behaviors:

  1. If it is used noninteractively (when the user presses the edit button), UIKit will be calledanimateTransition(using:)To set up the transition animation.
  2. If you use it interactively, UIKit will callinterruptibleAnimator(using:)Get the animator and use it to drive the transition.

Switch to the LockScreenViewController. Swift, add new method in UIViewControllerTransitioningDelegate extension:

func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    return presentTransition
}
Copy the code

Next, add two new properties to the LockScreenViewController to track the user’s gestures:

  var isDragging = false
  var isPresentingSettings = false
Copy the code

The isDragging flag is set to true when the user pulls down, and the isPresentingSettings is also set to true when the user pulls down.

A method to implement UISearchBarDelegate:

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    isDragging = true
}
Copy the code

This might seem a bit redundant, because the UITableView already has a property to track whether it is currently being dragged, but now it has to do some custom tracing itself.

We continue to implement another method of the UISearchBarDelegate protocol that tracks the user’s progress:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    guard isDragging else { return }

    if! isPresentingSettings && scrollView.contentOffset.y < -30 {
        isPresentingSettings = true
        presentTransition.wantsInteractiveStart = true
        presentSettings()
        return}}Copy the code

Next, you need to add code to update interactively. Append the following to the end of the above method:

if isPresentingSettings {
    let progess = max(0.0.min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))
    presentTransition.update(progess)
}
Copy the code

Calculate progress in the range of 0.0 to 1.0 based on the distance from the TableView, and call update(_:) on the transition animator to position the animation to the current progress. Run and see the table view blur as you drag down.

Note another way to implement the UISearchBarDelegate protocol to complete the cancel transition:

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    let progress = max(0.0.min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))

    if progress > 0.5 {
        presentTransition.finish()
    } else {
        presentTransition.cancel()
    }

    isPresentingSettings = false
    isDragging = false
}
Copy the code

This code looks similar to the 19- Interactive Navigation Controller transition. If the user pulls down more than half of the distance, the transition is considered successful. If the user does not pull down more than half, cancel the transition.

Replace the addCompletion block in the transitionAnimator(using:) method with:

    animator.addCompletion { (position) in
      switch position {
      case.end: transitionContext.completeTransition(! transitionContext.transitionWasCancelled)default:
        transitionContext.completeTransition(false)}}Copy the code

Run, pull up and down, and the following pixelation problem may occur (it may occur in iOS10, but should be fixed after iOS11) :

Use the auxAnimationsCancel property you added earlier in the PresentTransition. Find the call to animator.addcompletion in transitionAnimator(using:) and add the following:

self.auxAnimationsCancel? (a)Copy the code

To the presentSettings(_:) method of the LockScreenViewController. After setting the auxAnimations property, add:

presentTransition.auxAnimationsCancel = blurAnimations(false)
Copy the code

Run, the pixelation problem should have disappeared.

But there’s another problem. Clicking the Edit button on a non-interactive transition is not working! 😱

As soon as the user clicks the Edit button, the code needs to be changed to make the view controller transition non-interactive.

To the tableView of the LockScreenViewController (_:cellForRowAt:), insert before self.PresentSettings () :

self.presentTransition.wantsInteractiveStart = false
Copy the code

Operation, effect:

Interruptible transition animation

Next, consider switching between non-interactive and interactive modes during transitions.

In this section, the animation that displays the Settings controller begins when the Edit button is clicked, but pauses the transition if the user clicks the screen again during the animation.

Switch to PresentTranstion.swift. It required a slight change in the animator to handle not only interactive and non-interactive modes separately, but also the same transitions at the same time. Add two more properties to the PresentTranstion:

var context: UIViewControllerContextTransitioning?
var animator: UIViewPropertyAnimator?
Copy the code

Use these two properties to track the context of the animation as well as the animator. Before the return animator of the transitionAnimator(using:) method insert:

self.animator = animator
self.context = transitionContext
Copy the code

A reference to it is also stored each time a new animator is created for the transition.

It is also important to release these resources after the transition is complete. Continue to add:

animator.addCompletion { [unowned self] _  in
  self.animator = nil
  self.context = nil
}
Copy the code

Add one more method to PresentTranstion:

func interruptTransition(a) {
    guard let context = context else { return }
    context.pauseInteractiveTransition()
    pause()
}
Copy the code

Before the return animator of the transitionAnimator(using:) method insert:

animator.isUserInteractionEnabled = true
Copy the code

Make sure the transition animation is interactive so the user can continue interacting with the screen after pausing.

Allows the user to scroll up or down to complete or cancel the transition, respectively. To do this, add a new property to LockScreenViewController:

var touchesStartPointY: CGFloat? 
Copy the code

If the user touches the screen during a transition, it can pause and store the location of the first touch:

  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard presentTransition.wantsInteractiveStart == false, presentTransition.animator ! =nil else {
      return} touchesStartPointY = touches.first! .location(in: view).y
    presentTransition.interruptTransition()
  }
Copy the code

Tracking user touches and seeing if the user panned up or down, add:

 override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
  guard let startY = touchesStartPointY else { return }
  
  letcurrentPoint = touches.first! .location(in: view).y
  if currentPoint < startY - 40 {
    touchesStartPointY = nilpresentTransition.animator? .addCompletion({ (_) in
      self.blurView.effect = nil
    })
    presentTransition.cancel()
    
  } else if currentPoint > startY + 40 {
    touchesStartPointY = nil
    presentTransition.finish()
  }
}
Copy the code

Run, click the Edit button and immediately click the screen. At this time, the transition will be suspended. At this time, swipe down to complete the transition, and swipe up to cancel the transition.

System learning iOS Animation five: Using UIViewPropertyAnimator