- Building Fluid Interfaces: How to Create Natural Gestures and animations on iOS
- Originally written by Nathan Gitter
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: RydenSun
- Proofread by: Atuooo
How to create natural interactive gestures and animations on iOS
At WWDC 2018, Apple designers gave a talk titled “Designing Smooth Interfaces” to explain the design philosophy behind the iPhone X gesture interaction system.
WWDC18 Presentation “Design smooth Interface”
This is my favorite WWDC share — I highly recommend it
This share provides some technical guidance, which is unusual for a design talk, but it’s just pseudocode, leaving too much to be known.
Some code in the presentation that looks like Swift.
If you try to implement these ideas, you may find that there is a gap between ideas and implementation.
My goal is to close the gap by providing examples of working code for each major topic.
We’re going to create eight screens. Buttons, spring animations, custom interfaces and more!
Here’s an overview of what we’ll be talking about today:
- Summary of the presentation “Designing Smooth Interfaces”.
- 8 smooth interactive interfaces, the design philosophy behind and built code.
- Practical applications for designers and developers
What is a smooth interface?
A smooth interface can also be described as “fast”, “smooth”, “natural” or “fantastic”. It’s a smooth, frictionless experience where you just feel it’s right.
The WWDC presentation describes smooth interfaces as “an extension of your mind” or “an extension of the natural world.” When an interface does what people want it to do, rather than what machines want it to do, it is fluid.
What makes them flow?
Smooth interfaces are responsive, interruptible, and redirected. Here’s an example of an iPhone X gesture swiping back to the home page:
Applications can be closed during startup animation.
The interface responds instantly to user input, can be stopped in any process, and can even change the direction of the animation in mid-stream.
Why do we care about smooth interfaces?
- The smooth interface improves the user experience, making every interaction feel fast, lightweight and meaningful.
- They give users a sense of control, which builds trust in your app and your brand.
- They are hard to build. A smooth interface is hard to copy, which is a strong competitive advantage.
interface
In the remainder of this article, I will show you how to build the eight main interfaces mentioned in the WWDC talk.
The ICONS represent the eight interactive interfaces that we want to build.
Interface #1: Calculator button
This button mimics the behavior of buttons in the iOS Calculator app.
The core function
- Highlight immediately when clicked.
- It can be clicked instantly even while in animation.
- The user can cancel the tap when holding down the end of the gesture or when the finger comes off the button.
- The user can confirm the click by holding down the finger detach button and the finger return button at the end of the gesture.
Design concept
We want buttons to feel responsive and let the user know they are functional. In addition, we want the action to be undoable if the user decides to undo the action when the button is pressed. This allows users to make decisions faster because they can act on them while they are thinking about them.
Slides from the WWDC presentation show how gestures can be used simultaneously with ideas to make action faster.
The key code
The first step is to create a button that inherits from UIControl, not from UIButton. UIButton also works fine, but since we’re going to customize the interaction, we don’t need any of its functionality.
CalculatorButton: UIControl {public var value: Int = 0 {didSet {label.text = "\(value)"}} Private lazy var label: UILabel = { ... (1)}}Copy the code
Next, we’ll use UIControlEvents to assign responses to various click-interaction events.
addTarget(self, action: #selector(touchDown), for: [.touchDown, .touchDragEnter])
addTarget(self, action: #selector(touchUp), for: [.touchUpInside, .touchDragExit, .touchCancel])
Copy the code
We’re going to combine touchDown and touchDragEnter into a single event called touchDown, and we’re going to combine touchUpInside, touchDragExit and touchCancel into a single event, Is called a touchUp.
(See this document for a description of all the UIControlEvents available.)
This gives us two ways to handle animations.
private var animator = UIViewPropertyAnimator()
@objc private func touchDown() {
animator.stopAnimation(true)
backgroundColor = highlightedColor
}
@objc private func touchUp() {animator = UIViewPropertyAnimator(Duration: 0.5, curve:.easeout, animations: { self.backgroundColor = self.normalColor }) animator.startAnimation() }Copy the code
On touchDown, we unanimate the existing animation as needed, and immediately set the color to a highlight color (in this case, light gray).
In touchUp, we create a new animator and start the animation. You can easily unhighlight animations using UIViewPropertyAnimator.
Slide notes: This is not a serious representation of a button in the iOS Calculator app, which allows gestures to be moved from other buttons to this button to initiate a click event. For the most part, the buttons I’ve created here are the expected behavior of iOS buttons.)
Interface #2: Spring animation
This interaction shows how spring animations can be created by specifying a “damping” (bounce) and “response” (speed).
The core function
- Use design-friendly parameters.
- No concept of animation duration.
- Can be easily interrupted.
Design concept
The spring is a good animation model because of its speed and natural appearance. A spring animation can start extremely quickly, spending most of its time slowly approaching the final state. This is perfect for creating a responsive interface.
A few extra notes when designing spring animation:
- Spring animation does not need to be elastic. Using damping of value 1 builds an animation that slowly moves towards the rest without any bounce. Most animations should use damping with a value of 1.
- Try to avoid thinking about time. In theory, a spring animation is never completely close to the rest of the animation, and imposing a time limit would make the animation unnatural. Instead, keep adjusting the damping and response values until it feels right.
- Interruptibility is critical. Because spring animations consume most of their time approaching the final value, the user may assume that the animation is complete and will try to interact with it again.
The key code
In UIKit, we can build a spring animation with UIViewPropertyAnimator and a UISpringTimingParameters object. Unfortunately, it doesn’t have an initialization constructor that accepts only “damping” and “response.” The closest initialization constructor we can get is UISpringTimingParameters, which requires mass, hardness, damping, and initial acceleration.
UISpringTimingParameters(mass: CGFloat, stiffness: CGFloat, damping: CGFloat, initialVelocity: CGVector)
Copy the code
We want to create a simple initialization constructor that takes only the damping and response parameters and maps them to the desired mass, hardness, and damping.
Using a little physics, we can derive the notation we need:
Spring animation constant and damping coefficient solution.
With this result, we can create our own UISpringTimingParameters with exactly the parameters we want.
extension UISpringTimingParameters {
convenience init(damping: CGFloat, response: CGFloat, initialVelocity: CGVector = .zero) {
let stiffness = pow(2 * .pi / response, 2)
let damp = 4 * .pi * damping / response
self.init(mass: 1, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity)
}
}
Copy the code
This is how we can specify spring animation to all other interfaces.
The physics behind the spring animation
Want to dig deeper into spring animation? Check out this excellent article by Christian Schnorr: Demystifying UIKit Spring Animations.
After reading his article, I finally understood spring animation. Big hats off to Christian for helping me understand the mathematics behind these animations and teaching me how to solve second order differential equations.
Interface #3: Flashlight button
Another button, but in a different way. It mimics the flashlight button on the iPhone X lock screen.
The core function
- Requires a powerful gesture using 3D Touch.
- There is a rebound cue for gestures.
- There is vibration feedback to confirm start.
Design concept
Apple wanted to create a button that could be easily and quickly touched, but not accidentally triggered. The need to force the flashlight to start is a great option, but it lacks the visibility and feedback of the function.
To solve this problem, the button is flexible and grows as the user presses it. In addition, there are two separate tactile vibration feedback: one when the desired amount of pressure is applied, and the other when the end button is triggered. These senses of touch mimic the behavior of physical buttons.
The key code
To measure how hard the button is pressed, we can use the UITouch object provided by the Touch event.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let touch = touches.first else { return }
let force = touch.force / touch.maximumPossibleForce
let scale = 1 + (maxWidth / minWidth - 1) * force
transform = CGAffineTransform(scaleX: scale, y: scale)
}
Copy the code
We calculate the scaling based on how hard the user is pressing, so that the button gets bigger as the user presses it.
Since the button can be pressed but not activated, we need to keep track of the real-time status of the button.
enum ForceState {
case reset, activated, confirmed
}
private letResetForce: CGFloat = 0.4 privateletActivationForce: CGFloat = 0.5 privateletConfirmationForce: CGFloat = 0.49Copy the code
By setting the confirmation pressure to slightly less than the start pressure, it prevents the user from frequently starting and canceling the start button by quickly exceeding the pressure threshold.
For haptic feedback, we can use UIKit’s feedback generator.
private let activationFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
private let confirmationFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
Copy the code
Finally, for bounce animations, we can use UIViewPropertyAnimator and initialize the constructor with the UISpringTimingParameters we built earlier.
letParams = UISpringTimingParameters(damping: 0.4, response: 0.2)let animator = UIViewPropertyAnimator(duration: 0, timingParameters: params)
animator.addAnimations {
self.transform = CGAffineTransform(scaleX: 1, y: 1)
self.backgroundColor = self.isOn ? self.onColor : self.offColor
}
animator.startAnimation()
Copy the code
Interface #4: Rubber band animation
Rubber band animation occurs when the view resists movement. An example is when the scroll view slides to the bottom.
The core function
- The interface is always responsive, even when the operation is invalid.
- Asynchronous touch tracking, representing boundaries.
- As you move away from the boundary, you move less.
Design concept
Rubber band animation is a great way to communicate ineffective actions, and it still gives the user a sense of control. It gently tells you that this is a boundary and pulls them back to their valid state.
The key code
Fortunately, rubber band animation is straightforward to implement.
Offset (offset, 0.7) = powCopy the code
By using an exponent between 0 and 1, the view moves less and less as it moves away from its original position. If you want to move less, use a big exponent, and if you want to move more, use a small exponent.
In more detail, this code is typically implemented in the UIPanGestureRecognizer callback when the touch moves.
var offset = touchPoint.y - originalTouchPoint.y offset = offset > 0 ? Pow (offset, 0.7) : -pow(-offset, 0.7) view.transform = CGAffineTransform(0, y: offset)Copy the code
Note: This is not how Apple uses elements like Scroll View to animate rubber bands. I like this method because it’s simple, but there are many more complicated methods for different representations.
Interactive Interface #5: Accelerated abort
To see the app switch on the iPhone X, users swipe up from the bottom of the screen and stop halfway through. The interface is designed to create this representation.
The core function
- The stop is calculated based on gesture acceleration.
- Faster stops result in faster responses.
- No timer.
Design concept
A smooth interface should be fast. A timer delay, even a short one, can make the interface feel sluggish.
This interaction is cool because the reaction time is based on the user’s gestures. If they stop quickly, the interface responds quickly. If they slowly stop, the interface slowly responds.
The key code
To measure acceleration, we can track the speed value of the latest drag gesture.
private var velocities = [CGFloat]()
private func track(velocity: CGFloat) {
if velocities.count < numberOfVelocities {
velocities.append(velocity)
} else {
velocities = Array(velocities.dropFirst())
velocities.append(velocity)
}
}
Copy the code
This code updates the array so that it is always up to date with the seven velocity values, which can be used to calculate the acceleration values.
To determine if the acceleration is large enough, we can calculate the difference between the first velocity value in the array and the current velocity value.
if abs(velocity) > 100 || abs(offset) < 50 { return }
let ratio = abs(firstRecordedVelocity - velocity) / abs(firstRecordedVelocity)
ifThewire > 0.9 {pauseLabel. Alpha = 1 feedbackGenerator. ImpactOccurred hasPaused = ()true
}
Copy the code
We also want to make sure that gesture movement has a minimum displacement and speed. If the gesture has slowed down by more than 90%, we consider stopping it.
My implementation isn’t perfect. In my tests, it seemed to work well, but there was still a chance to explore the calculation of acceleration.
Interactive interface #6: Reward animations with self-momentum with some rebound effects
A drawer animation, with open and closed states, they will have some bounce depending on the speed of the gesture.
The core function
- Click on drawer animation, no bounce.
- Light pop-up drawer, there is a rebound.
- Interactive. interruptible and reversible.
Design concept
Drawer animation illustrates the concept of this interactive interface. When the user has a certain speed to slide a view, it is more satisfying to animate it with some bounce. The interface feels alive and more fun.
When the drawer is clicked, its animation does not bounce, which feels right because there is no clear directional momentum when clicked.
When designing custom interfaces, keep in mind that interfaces have different animations for different interactions.
The key code
To simplify the logic of clicking and dragging gestures, we can use a custom subclass of a gesture recognizer that enters the Began state the moment you click.
class InstantPanGestureRecognizer: UIPanGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
self.state = .began
}
}
Copy the code
This allows the user to stop the drawer by clicking on it while it is moving, which is like clicking on a scrolling view while it is scrolling. To handle these clicks, we can check if the speed is zero when the gesture stops and continue the animation.
if yVelocity == 0 {
animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
}
Copy the code
To process a gesture with speed, we first need to calculate its speed relative to the total distance remaining.
let fractionRemaining = 1 - animator.fractionComplete
let distanceRemaining = fractionRemaining * closedTransform.ty
if distanceRemaining == 0 {
animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
break
}
let relativeVelocity = abs(yVelocity) / distanceRemaining
Copy the code
When we can use this relative speed, in conjunction with the timing variable, we continue the animation that contains a bit of bounce.
letTimingParameters = UISpringTimingParameters(damping: 0.8, response: 0.3, initialVelocity: CGVector(dx: relativeVelocity, dy: relativeVelocity))let newDuration = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters).duration
let durationFactor = CGFloat(newDuration / animator.duration)
animator.continueAnimation(withTimingParameters: timingParameters, durationFactor: durationFactor)
Copy the code
Here we create a new UIViewPropertyAnimator to calculate how long the animation takes so that we can provide the correct durationFactor when continuing the animation.
As for the rotation of the animation, it will be more complicated, so I won’t introduce it here. If you want to know more, I wrote a full tutorial on this part: Building Better iOS APP Animations.
Interactive animation #7: FaceTime PiP
Re-create the Picture-in-Picture UI in the iOS FaceTime app.
The core function
- Lightweight, light interaction
- Projection position is based on
UIScrollView
Deceleration rate of. - There is a continuous animation that follows the initial speed of the gesture.
The key code
Our ultimate goal is to write some code like this.
letParams = UISpringTimingParameters(damping: 1, response: 0.4, initialVelocity: relativeInitialVelocity)let animator = UIViewPropertyAnimator(duration: 0, timingParameters: params)
animator.addAnimations {
self.pipView.center = nearestCornerPosition
}
animator.startAnimation()
Copy the code
We want to create an animation with an initial speed that matches the speed of the drag gesture. And PIP animation to the nearest corner.
First, we need to calculate our initial velocity.
In order to do that, we need to calculate the relative velocity based on our current velocity, so far, and our goal.
let relativeInitialVelocity = CGVector(
dx: relativeVelocity(forVelocity: velocity.x, from: pipView.center.x, to: nearestCornerPosition.x),
dy: relativeVelocity(forVelocity: velocity.y, from: pipView.center.y, to: nearestCornerPosition.y)
)
func relativeVelocity(forVelocity velocity: CGFloat, from currentValue: CGFloat, to targetValue: CGFloat) -> CGFloat { guard currentValue - targetValue ! = 0else { return0}return velocity / (targetValue - currentValue)
}
Copy the code
We can divide the velocities into x and y components and determine their relative velocities.
Next, we calculate the corners for the PiP animation.
To make our interface feel natural and lightweight, we projected the final PiP position based on its current movement. If a PiP can slide and stop, where does it end up?
let decelerationRate = UIScrollView.DecelerationRate.normal.rawValue
let velocity = recognizer.velocity(in: view)
let projectedPosition = CGPoint(
x: pipView.center.x + project(initialVelocity: velocity.x, decelerationRate: decelerationRate),
y: pipView.center.y + project(initialVelocity: velocity.y, decelerationRate: decelerationRate)
)
let nearestCornerPosition = nearestCorner(to: projectedPosition)
Copy the code
We can use the deceleration rate of UIScrollView to calculate the remaining positions. This is important because it relates to the user’s muscle memory for sliding. If a user knows how far a view needs to scroll, they can use their previous knowledge to intuitively guess how hard a PiP will need to be to reach the final goal.
The deceleration rate is also wide enough to make the interaction feel lightweight — a small push is all it takes to send a PiP across the screen.
We can calculate the final projection position using the projection method from the talk “Designing smooth Interfaces”.
/// Distance traveled after decelerating to zero velocity at a constant rate.
func project(initialVelocity: CGFloat, decelerationRate: CGFloat) -> CGFloat {
return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate)
}
Copy the code
The last missing piece is the logic of finding the nearest corner based on the projection position. We can loop through all the corners and find a corner that is the least distant from the projected position.
func nearestCorner(to point: CGPoint) -> CGPoint {
var minDistance = CGFloat.greatestFiniteMagnitude
var closestPosition = CGPoint.zero
for position in pipPositions {
let distance = point.distance(to: position)
if distance < minDistance {
closestPosition = position
minDistance = distance
}
}
return closestPosition
}
Copy the code
To summarize the final implementation: we use the deceleration rate of UIScrollView to project the PIP’s motion to its final position, and calculate the relative velocity and pass in UISpringTimingParameters.
Interface #8: Rotation
Apply PiP’s principles to a rotating animation.
The core function
- Use projections to follow the speed of gestures.
- Always stop in a valid direction.
The key code
The code here is very similar to the previous PiP. We will use the same construction callback, except replace the nearestCorner method with closestAngle.
func project(...) {... } func relativeVelocity(...) {... } func closestAngle(...) {... }Copy the code
When it’s finally time to create a UISpringTimingParameters, for the initial speed, we need to use a CGVector, even if our rotation only has one dimension. In any case, if the animation property has only one dimension, set the dx value to the desired speed and the dy value to 0.
letTimingParameters = UISpringTimingParameters(damping: 0.8, response: 0.4, initialVelocity: CGVector(dx: relativeInitialVelocity, dy: 0) )Copy the code
Internally, the animation ignores the value of dy and uses the value of dx to create a time curve.
Try it yourself!
These interactions are more interesting on a real phone. To play with these interactions for yourself, this is a demo app available on GitHub.
Smooth interactive interface demo application, can be obtained on GitHub!
The practical application
For designers
- Think of the interface as an expressive mediator of the process, rather than as a combination of fixed elements.
- Consider animations and gestures early in the design process. Sketch is a great typography tool, but it doesn’t provide the same complete performance as a device.
- Prototype with development engineers. Let design-minded developers help you develop prototypes for animation, gesture, and haptic feedback.
For development engineers
- Apply these suggestions to the custom interaction components you develop yourself. Think about how to combine them in a more interesting way.
- Tell your designer about these new possibilities. Many designers don’t realize the true power of 3D touch, haptic feedback, gestures and spring animation.
- Demonstrate the prototype with the designer. Help them see their designs on a real machine, and create tools to help them make them more efficient.
If you enjoyed this post, please leave a few claps. 👏 👏 👏
You can click and clap 50 times! So hurry up! 😉
Please share this article with your iOS designer /iOS developer friends on social media.
If you like this kind of content, you should follow me on Twitter. I only post high quality content. Twitter.com/nathangitte…
Thanks to David Okun for proofreading.
Thanks to Christian Schnorr and David Okun.
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.