📜 ⬆️ ⬇️

Use UIViewPropertyAnimator to create custom animations

Creating animations is great. They are an important part of the iOS Human Interface Guidelines . Animations help draw the user's attention to important things, or simply make the application not so boring.

There are several ways to implement animation in iOS. Probably the most popular way is to use UIView.animate (withDuration: animations :) . You can animate the image layer using CABasicAnimation . In addition, UIKit allows you to customize the animation for displaying the controller using UIViewControllerTransitioningDelegate .

In this article I want to discuss another exciting way to animate views - UIViewPropertyAnimator . This class provides much more control functions than its predecessor UIView.animat e. With it, you can create temporary, interactive and interruptible animations. In addition, it is possible to quickly change the animator.

Meet UIViewPropertyAnimator


UIViewPropertyAnimator was introduced in iOS 10 . It allows you to create animations in an object-oriented way. Let's look at an example of an animation created using a UIViewPropertyAnimator .
')
image

This is how it was when using UIView.

UIView.animate(withDuration: 0.3) { view.frame = view.frame.offsetBy(dx: 100, dy: 0) } 

And this is how it can be done with the help of UIViewPropertyAnimator :

 let animator = UIViewPropertyAnimator(duration:0.3, curve: .linear) { view.frame = view.frame.offsetBy(dx:100, dy:0) } animator.startAnimation() 

If you need to check the animation, just create a Playground and run the code as shown below. Both code fragments will lead to the same result.

image

You would think that in this example there is not much difference. So, what's the point of adding a new way to create animation? UIViewPropertyAnimator becomes more useful when you need to create interactive animations.

Interactive and interruptible animation


Do you remember the classic “Shift finger to unlock device” gesture? Or the gesture “Move your finger on the screen from bottom to top” to open the Control Center? These are excellent examples of interactive animation. You can start moving the image with your finger, then release it, and the image will return to its original position. In addition, you can catch the image during the animation and continue moving it with your finger.

Animations UIView does not provide an easy way to control the percentage of completion of an animation. You cannot pause the animation in the middle of a loop and continue its execution after the interruption.

In this case, it will be a UIViewPropertyAnimator . Next, we will look at how you can easily create a fully interactive, interrupted animation, and reversing animation in a few steps.

Preparation of the starting project


First you need to download the start project . After opening the archive, you will find the CityGuide application that helps users plan their vacation. The user can scroll through the list of cities, and then open a detailed description with detailed information about the city that he liked.

Consider the source code of the project before we start creating a beautiful animation. Here's what you can find in the project by opening it in Xcode :

  1. ViewController.swift : The main application controller with a UICollectionView that displays an array of City objects.
  2. CityCollectionViewCell.swift: Cell for displaying City . In fact, in this article most of the changes will apply to this class. You may notice that descriptionLabel and closeButton are already defined in the class. However, after launching the application, these objects will be hidden. Do not worry, they will be seen a little later. In this class there are also collectionView and index properties. Later they will be used for animation.
  3. CityCollectionViewFlowLayout.swift: This class is responsible for horizontal scrolling. We will not change it yet.
  4. City.swift : The main application model has a method that was used in the ViewController.
  5. Main.storyboard: There you can find the user interface for the ViewController and CityCollectionViewCell .

Let's try to build and run the sample application. As a result, we get the following.

cityguideapp-iphone8

Implement deployment and collapse animation


After starting the application, a list of cities is displayed. But the user can not interact with objects in the form of cells. Now you need to display information for each city when the user clicks on one of the cells. Take a look at the final application. That's actually what was required to develop:

image

The animation looks good, doesn't it? But there is nothing special here, it's just the basic logic of the UIViewPropertyAnimator . Let's see how to implement this type of animation. Create a collectionView method (_: didSelectItemAt) , add the following code fragment to the end of the ViewController file:

 func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let selectedCell = collectionView.cellForItem(at: indexPath)! as! CityCollectionViewCell selectedCell.toggle() } 

Now we need to implement the toggle method. Let's switch to CityCollectionViewCell.swift and implement this method.

First, we add the State enumeration to the beginning of the file, just before the CityCollectionViewCell class declaration . This listing allows you to track the state of a cell:

 private enum State { case expanded case collapsed var change: State { switch self { case .expanded: return .collapsed case .collapsed: return .expanded } } } 

Add a few properties to control the animation in the CityCollectionViewCell class:

 private var initialFrame: CGRect? private var state: State = .collapsed private lazy var animator: UIViewPropertyAnimator = { return UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) }() 

The initialFrame variable is used to store the cell frame before performing the animation. state is used to track if a cell is expanded or collapsed. And the variable animator is used to control the animation.

Now add the toggle method and call it from the close method, for example:

 @IBAction func close(_ sender: Any) { toggle() } func toggle() { switch state { case .expanded: collapse() case .collapsed: expand() } } 

Then add two more methods: expand () and collapse () . We will continue their implementation. First we start with the expansiond () method:

 private func expand() { guard let collectionView = self.collectionView, let index = self.index else { return } animator.addAnimations { self.initialFrame = self.frame self.descriptionLabel.alpha = 1 self.closeButton.alpha = 1 self.layer.cornerRadius = 0 self.frame = CGRect(x: collectionView.contentOffset.x, y:0, width: collectionView.frame.width, height: collectionView.frame.height) if let leftCell = collectionView.cellForItem(at: IndexPath(row: index - 1, section: 0)) { leftCell.center.x -= 50 } if let rightCell = collectionView.cellForItem(at: IndexPath(row: index + 1, section: 0)) { rightCell.center.x += 50 } self.layoutIfNeeded() } animator.addCompletion { position in switch position { case .end: self.state = self.state.change collectionView.isScrollEnabled = false collectionView.allowsSelection = false default: () } } animator.startAnimation() } 

Like a lot of code. Let me explain what is happening step by step:

  1. First we check if collectionView and index are not equal to zero. Otherwise, we will not be able to start the animation.
  2. Next, we start creating an animation by calling animator.addAnimations .
  3. Next, save the current frame, which is used to restore it in the collapse animation.
  4. Then we set the alpha value for the descriptionLabel and closeButton to make them visible.
  5. Next, remove the rounded corner and set a new frame for the cell. The cell will be shown in full screen mode.
  6. Next, we move the neighboring cells.
  7. Now we call the animator.addComplete () method to disable the interaction of the image collection. This prevents users from scrolling during cell expansion. Also change the current state of the cell. It is important to change the state of the cell and only after that the animation ends.

Now add a collapse animation. In short, it’s just that we restore the cell to its former state:

 private func collapse() { guard let collectionView = self.collectionView, let index = self.index else { return } animator.addAnimations { self.descriptionLabel.alpha = 0 self.closeButton.alpha = 0 self.layer.cornerRadius = self.cornerRadius self.frame = self.initialFrame! if let leftCell = collectionView.cellForItem(at: IndexPath(row: index - 1, section: 0)) { leftCell.center.x += 50 } if let rightCell = collectionView.cellForItem(at: IndexPath(row: index + 1, section: 0)) { rightCell.center.x -= 50 } self.layoutIfNeeded() } animator.addCompletion { position in switch position { case .end: self.state = self.state.change collectionView.isScrollEnabled = true collectionView.allowsSelection = true default: () } } animator.startAnimation() } 

Now it's time to compile and run the application. Try clicking on a cell and you will see an animation. To close the image, click on the cross icon in the upper right corner.

Adding Gesture Processing


You can argue about achieving the same result using UIView.animate . What is the point of using UIViewPropertyAnimator ?

Well, it's time to make the animation interactive. Add a UIPanGestureRecognizer and a new property named popupOffset to track how much a cell can move. Let's declare these variables in the CityCollectionViewCell class:

 private let popupOffset: CGFloat = (UIScreen.main.bounds.height - cellSize.height)/2.0 private lazy var panRecognizer: UIPanGestureRecognizer = { let recognizer = UIPanGestureRecognizer() recognizer.addTarget(self, action: #selector(popupViewPanned(recognizer:))) return recognizer }() 

Then add the following method to register the svip definition:

 override func awakeFromNib() { self.addGestureRecognizer(panRecognizer) } 

Now you need to add the popupViewPanned method to track the swipe gesture. Paste the following code into CityCollectionViewCell :

 @objc func popupViewPanned(recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: toggle() animator.pauseAnimation() case .changed: let translation = recognizer.translation(in: collectionView) var fraction = -translation.y / popupOffset if state == .expanded { fraction *= -1 } animator.fractionComplete = fraction case .ended: animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) default: () } } 

There are three states here. At the beginning of the gesture, we initialize the animator with the toggle method and immediately suspend it. While the user drags the cell, we update the animation by setting the fractionComplete multiplier properties. This is the main magic of the animator, which allows them to manage. Finally, when the user releases the finger, the continueAnimation animator method is called to continue the animation. The cell then moves to the target position.

After starting the application, you can drag the cell up to expand it. Then drag the expanded cell down to collapse it.

Now the animation looks pretty good, but it's not possible to interrupt the animation in the middle. Therefore, to make the animation fully interactive, you need to add another function - interrupt. The user can start the expand / collapse animation as usual, but the animation should be paused immediately after the user clicks the cell during the animation cycle.

To do this, you need to save the progress of the animation and then take this value into account to calculate the percentage of completion of the animation.

First, let's declare a new property in CityCollectionViewCell :

 private var animationProgress: CGFloat = 0 

Then we update the .began block of the popupViewPanned method with the following line of code to remember the progress:

 animationProgress = animator.fractionComplete 

In the .changed block, you need to update the following line of code to correctly calculate the percentage of completion:

 animator.fractionComplete = fraction + animationProgress 

Now the application is ready for testing. Run the project and see what happens. If all actions are performed correctly following my instructions, the animation should look like this:

image

Reverse animation


You can find a flaw for the current implementation. If you drag the cell a little and then return it to its original position, the cell will continue to expand when you release your finger. Let's fix this problem to make interactive animations even better.
Perform an update to the .end block of the popupViewPanned method, as described below:

 let velocity = recognizer.velocity(in: self) let shouldComplete = velocity.y > 0 if velocity.y == 0 { animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) break } switch state { case .expanded: if !shouldComplete && !animator.isReversed { animator.isReversed = !animator.isReversed } if shouldComplete && animator.isReversed { animator.isReversed = !animator.isReversed } case .collapsed: if shouldComplete && !animator.isReversed { animator.isReversed = !animator.isReversed } if !shouldComplete && animator.isReversed { animator.isReversed = !animator.isReversed } } animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) 

We now take into account the speed of the gesture to determine if the animation should be reversed.

And finally, insert another line of code into the .changed block. Place this code to the right of the calculation of animator.fractionComplete .

 if animator.isReversed { fraction *= -1 } 

Let's run the application again. Now everything should work without fail.

image

Fix pan gesture


So, we have completed the implementation of the animation using the UIViewPropertyAnimator . However, there is one unpleasant mistake. You may have met her while testing the application. The problem is that scrolling the cell horizontally is not possible. Let's try to swipe left / right in the cells, and we dove with the problem.

The main reason is related to the UIPanGestureRecognizer we created . It also captures the brush gesture, and conflicts with the built-in gesture recognizer UICollectionView .

Although the user can still scroll through the top / bottom of the cells or the space between the cells to scroll through the cities, I still don’t like such a bad user interface. Let's fix it.

To resolve conflicts, we need to implement a delegate method named gestRecognizerShouldBegin (_ :) . This method controls whether the gesture recognizer should continue to interpret the touches. If you return false to the method, the gesture recognizer will ignore touches. So what we are going to do is give our own panorama recognition tool the ability to ignore horizontal movements.

To do this, let's set the delegate of our pan recognizer. Insert the following line of code into the panRecognizer initialization (you can put the code right before return recognizer :

 recognizer.delegate = self 

Then, we implement the gestRecognizerShouldBegin (_ :) method as follows:

 override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { return abs((panRecognizer.velocity(in: panRecognizer.view)).y) > abs((panRecognizer.velocity(in: panRecognizer.view)).x) } 

We will open / close if its speed of vertical movement is greater than the speed of horizontal movement.

Great! Let's test the application again. Now you can navigate through the list of cities by swiping left / right through the cells.

image

Bonus: Custom sync features


Before we finish this tutorial, let's talk about timing functions. Do you still remember the case when the developer asked you to implement a custom synchronization function for the animation you create?

You should usually change UIView.animation to CABasicAnimation or wrap it in CATransaction . With UIViewPropertyAnimator, you can easily implement custom timing functions.

The timing functions (or easing functions) are understood as functions of the animation speed, which affect the rate of change of a particular property being animated. Four types are currently supported: easeInOut, easeIn, easeOut, linear.

Replace the animator's initialization of this timing functions (try drawing your own cubic Bezier curve) as follows:

 private lazy var animator: UIViewPropertyAnimator = { let cubicTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.17, y: 0.67), controlPoint2: CGPoint(x: 0.76, y: 1.0)) return UIViewPropertyAnimator(duration: 0.3, timingParameters: cubicTiming) }() 

Alternatively, instead of using the cubic sync parameters, you can also use spring synchronization, for example:

  let springTiming = UISpringTimingParameters(mass: 1.0, stiffness: 2.0, damping: 0.2, initialVelocity: .zero) 

Try running the project again and see what happens.

Conclusion


With UIViewPropertyAnimator, you can enhance static screens and user interaction with interactive animations.

I know that you cannot wait to implement what you have learned in your own project. If you apply this approach in your project, it will be very cool, let me know about it, leaving a comment below.

As a reference material, you can download the final draft .

Further links


Professional animations using UIKit - https://developer.apple.com/videos/play/wwdc2017/230/

UIViewPropertyAnimator Apple Developer Documentation - https://developer.apple.com/documentation/uikit/uiviewpropertyanimator

Source: https://habr.com/ru/post/458970/


All Articles