Streaming is available in most browsers,
and in the Developer app.
-
Wind your way through advanced animations in SwiftUI
Discover how you can take animation to the next level with the latest updates to SwiftUI. Join us as we wind our way through animation and build out multiple steps, use keyframes to add coordinated multi-track animated effects, and combine APIs in unique ways to make your app spring to life.
Chapters
- 0:00 - Introduction
- 2:23 - Animation phases
- 8:12 - Keyframes
- 15:07 - Tips and tricks
Resources
Related Videos
WWDC23
-
Download
♪ ♪ Person: Hello, and welcome to Wind your way through advanced animations in SwiftUI. I’m Tim, a member of the SwiftUI team.
SwiftUI comes with a powerful set of animation tools that make your app shine, with animations that are interruptible, physics-based for believable motion, and deeply integrated throughout the framework.
Today we’re going to talk about some exciting new tools that let you take animation in your app to the next level. Before we begin, let’s do a quick review of the animation tools that you already know in SwiftUI.
You might have seen this app that allows you to vote for your favorite kind of pet in other sessions.
To simplify this demo, I went ahead and removed all of the other options, because cats are clearly the best choice.
Adding animation to your app is as easy as using "withAnimation" or adding an "animation" modifier, which gives you great behavior out of the box.
After the state of your application changes, SwiftUI applies animations that interpolate from the previous state to the new state.
But with animation, as with life, sometimes the most rewarding experiences are found when you aren’t so focused on where you came from or where you’re going.
Sometimes you have to get off of the beaten path and focus on the journey itself to make something special. And some animations don’t simply animate from a previous state into a new state.
Today, I’ll talk about some powerful new tools for building complex, multistep animations.
Rather than animating between two states, these animations can define multiple steps that happen in sequence. And they are especially great in two situations: repeating animations, that loop continuously while a view is visible... and event-driven animations, such as a view that pulses when an event occurs.
In this talk, I’m going to introduce a new family of APIs that makes animations like these even easier to build.
I’ll start by introducing you to animation phases, which let SwiftUI automatically advance through a set of pre-planned states that make up your animation.
Next, I’ll demonstrate how to take animations even further with keyframes. And finally, I’ll show some advanced tips and tricks to get the most out of this API. I think we’re ready to go. Let’s jump right in.
When I’m not writing Swift, I like to get out trail running.
Trail races can be very long. Ultramarathons can take a whole day, or even multiple days to finish, so I’ve been building an app to plan for upcoming events, and to help me remember important details during a run.
Nutrition is super important when you’re on the trail.
Unfortunately, it can be easy to forget to eat late in a race as exhaustion sets in.
I’ve added a feature to my app that reminds me to eat at the right times.
Here, the reminder at the bottom of the screen is letting me know that I’m overdue for a meal. But there’s a problem.
Later in a race, I can get so tired that I could miss a subtle indicator like this.
I really don’t want to accidentally skip a meal, so I’ll add some motion to make this reminder stand out.
Let’s focus on this one view. We want to give it an animated highlight effect to make it extra visible.
To make this view animate, we will apply the ".phaseAnimator" modifier.
When you use the phase animator modifier, you provide a sequence of states that define the individual steps in a multipart animation.
SwiftUI then animates between these states automatically.
In this case, we’ll just be animating between two states: highlighted, and not highlighted, so we can simply use boolean values.
Next, we’ll apply some modifiers to change the appearance of the view depending on the current phase.
We’ll start with an opacity modifier: we’ll make the view fully opaque when highlighted, and 50% transparent otherwise.
And right away, the view starts animating. Let’s talk about what SwiftUI is doing on your behalf.
In our view, we provided two phases to the phase animator modifier: false, and true.
When the view first appears, the first phase is active, causing the view to be 50% transparent.
SwiftUI then immediately begins an animated transition to the next phase, where the view is fully opaque.
Then when that animation is finished, SwiftUI advances again. We only have two phases, so we loop around to the beginning.
This causes our animation to cycle between the two states. Of course, you can also define animations that include more than two phases, and any number of additional view modifiers, which I'll demonstrate later on.
Now while the view is animating, the effect is really subtle.
Instead of changing the opacity, let’s try changing the foreground style.
We’ll use red when highlighted, and otherwise fall back to the primary foreground style.
And that’s much more visible. The animations are a bit abrupt, though.
By default, SwiftUI uses a spring animation.
And while springs are great for handling dynamic state changes, in this case we want a smoother, more consistent animation.
We can change the animation by adding a trailing "animation" closure.
The phase that is being animated to is passed in, in case we want to use a different animation for each phase. But in this case, I always want to use the same ease in out animation with a custom duration, to slow things down.
Now, you wouldn’t typically use an animation with a duration of a full second for an interactive state change because you wouldn’t want to make people wait for an animation to finish.
But in this case, we are building an ambient effect, so it’s ok for things to move a bit slower, just like my pace if I miss that meal.
Now that we’ve solved the urgent problem of mid-race nutrition, let’s look at one more way to use animation phases: animations that are triggered by events.
I’ve been working on my app for a while, and I’ve added the ability to see which races my friends have run.
The emoji show reactions left by others.
Every runner sometimes asks themselves: why do I do this? Why did I sign up to run so many miles? And the least that our app can do is to feed the need for external validation by adding some excitement when someone else likes a race.
We’ll add an animation that plays every time someone adds a reaction.
The first thing that we will do is define the phases of our animation.
Unlike in the previous example that simply alternated between two states, we want a more complex animation. An enum is a great way to define a list of steps for the animation.
We’ve added three cases: a case for the initial appearance, then cases to move the view up, and scale up the view.
To simplify our view body, we will add computed properties to this enum that define the different effects that we will apply. I want the view to jump up during the animation, so I’ve added a computed vertical offset property.
I switch over the enum to return the right offset for each case.
Likewise, I’ve added two additional computed properties to determine the view’s scale and foreground style.
I won’t show the implementations here, but they also use a switch statement, just like the vertical offset property. Now, let’s get back to our view and add the animation.
We add the phaseAnimator modifier, but this time, we give it a "trigger" value.
When we give the phase animator modifier a trigger value, it observes the value that you specify for changes.
And when a change occurs, it begins animating through the phases that you specify.
Using the computed properties that we defined on the phase type, we apply modifiers to the view.
And this animation technically does the right thing, but it doesn’t feel great.
It’s a bit sluggish.
We’ll customize the animation for each transition to get the effect that we want, including a couple of different spring animations.
And this looks much better! But what if we want to take this animation even further? When someone has finished 50 or 100 miles on the trail, we want to give them an animation that leaves no doubt in their mind that all of those miles were worth it when they receive some well-deserved kudos. When you need even more control, there’s another powerful tool: keyframes. Next, I’ll show you how to use keyframes to define complex, coordinated animations with complete control over timing and movement. First, let’s talk about how keyframes are different from the phases that we have used so far.
Phases define discrete states that are provided to your view one at a time.
And SwiftUI animates between those states, using the same animation types that you already know, and this works really well for animations that can be modeled as discrete states.
When a state transition occurs, all of the properties are animated at the same time.
And then, when that animation is finished, SwiftUI animates to the next state.
And this continues across all of the phases of the animation.
But what if we want to animate each property independently? That’s where keyframes come in.
Keyframes let you define values at specific times within an animation. To demonstrate, I’ll animate this view, starting with a rotation effect.
The dots here indicate keyframes: angles to use at each point during the animation.
When the animation plays back, SwiftUI interpolates values in between these keyframes, which we can then use to apply modifiers to the view.
And keyframes allow you to independently animate multiple effects at the same time by defining separate tracks, each with their own unique timing.
This is really powerful, because you can use keyframes to drive any modifier in SwiftUI.
In this example, we are using keyframes to drive several other tracks, including vertical stretch, scale, and translation.
Let’s get back to our view and see what this looks like in code.
I already have an idea of the animation that I want to build, so my first step is to define the properties that will drive the animation. To do this, I’ll create a new struct containing all of the different properties that will be independently animated.
Keyframes can animate any value conforming to the "Animatable" protocol.
Notice that several properties use "Double", which now conforms to "Animatable." Unlike phases, in which you model separate, discrete states, keyframes generate interpolated values of the type that you specify.
While an animation is in progress, SwiftUI will provide you with a value of this type on every frame so that you can update the view.
Next, we add the keyframeAnimator modifier.
This modifier is similar to the phase animator that we used earlier, but accepts keyframes.
Notice that we provide an instance of our struct to use as the initial value.
The keyframes that we define will apply animations onto this value. Next, we’ll apply modifiers to our view for each of the properties on the struct.
And finally, we’ll start defining keyframes.
As I mentioned, keyframes let you build sophisticated animations with different keyframes for different properties. To make this possible, keyframes are organized into tracks. Each track controls a different property of the type that you are animating, which is specified by the key path that you provide when creating the track. Here, we are adding keyframes for the scale property.
We first add a linear keyframe, repeating the initial scale value and holding it for 0.36 seconds. And if you’re wondering how I settled on 0.36, I found that duration by trying out different values to change the feel of the animation, and that’s an important point about keyframes. Making an animation that suits your app can take some experimentation.
Previews in Xcode can be a great way to fine-tune your animations. Next, we add a "SpringKeyframe." This uses a spring function to pull the value toward the target.
And we’ve specified a duration.
For a spring keyframe with a set duration, this means that the spring function will only animate the value for that duration.
After that, interpolation will begin to the next keyframe. Finally, I’ll add another spring keyframe that animates the scale back to 1.0. The different kinds of keyframes control how values are interpolated. Alrighty, we’ve seen LinearKeyframe and SpringKeyframe.
There are actually four different types of keyframes. I’ll explain how they are different: LinearKeyframe interpolates linearly in vector space from the previous keyframe.
SpringKeyframe, as its name suggests, uses a spring function to interpolate to the target value from the previous keyframe. CubicKeyframe uses a cubic Bézier curve to interpolate between keyframes. If you combine multiple cubic keyframes in sequence, the resulting curve is equivalent to a Catmull-Rom spline.
And finally, MoveKeyframe immediately jumps to a value without interpolation. Each kind of keyframe supports customization to give you full control, and you can mix and match different kinds of keyframes within an animation.
SwiftUI maintains velocity between keyframes to make sure your animation remains continuous.
Coming back to our view, we’re ready to add the next track.
Here, we’ve used linear and spring keyframes to animate the vertical translation.
Right before the view jumps up, it pulls back in anticipation.
We have modeled that with a spring keyframe that pulls the view down briefly before it moves up. This is looking good, but we still have two more properties to animate: vertical stretch, and rotation.
We’ll start with vertical stretch, and for this, we will use cubic keyframes. Again, this can take some trial and error to get right, but don’t hesitate to experiment with different ways to model animations using keyframes.
The squash and stretch really gives this animation a lot more energy.
Finally, we’ll animate the rotation as well. And this is looking great.
And those curves that we saw earlier? Those are a visualization of the animation that we just built. You can add additional tracks to apply any SwiftUI modifier.
I’ve had a lot of fun exploring different combinations.
let’s take a moment to review the model of keyframes. Keyframes are predefined animations.
That means that they are not a replacement for normal SwiftUI animations in situations where the UI should be fluid and interactive.
Instead, think of keyframes like video clips that can be played. They give you a ton of control, but there’s a tradeoff.
Because you specify exactly how an animation should progress, keyframe animations can’t gracefully retarget the way that springs can, so it’s generally best to avoid changing keyframes mid-animation.
Keyframes animate a value of the type that you define, which you then use to apply modifiers to the view. You can use a single keyframe track to drive a single modifier, or a combination of different modifiers.
It’s up to you.
And because the animation happens in terms of the value that you define, updates happen on every frame, so you should avoid performing any expensive operations while applying a keyframe animation to the view.
Finally, I’ll demonstrate how how you can do even more with keyframes.
My app includes a race map, showing the route that each leg takes.
I want to add an animation that automatically zooms in and follows the course.
Thankfully, MapKit now allows me to use keyframes to move the camera. Here, I’m using a "Map" view to show the course.
My view already has a route, which is a model that contains all of the coordinates along one leg of the race.
To build our tour, we’ll add a state property and a button to change it.
Finally, we use the new "mapCameraKeyframeAnimator" modifier. We give it the trigger value, then add keyframes, just like we used for the heart icon in the previous example.
Every time the trigger value changes, maps will use these keyframes to animate.
The final value of the keyframes determines the camera value that is used at the end of the animation.
Finally, we hit the button, and the tour starts.
If the user performs a gesture while animating, the animation will be removed and the user will have full control over the camera.
By independently animating the center coordinate, heading, and distance, we’re able to smoothly animate along this course then zoom back out for a bird’s-eye view.
Finally, I’d like to demonstrate how keyframes can be manually evaluated to drive any kind of effect that you can think of.
We’ve seen the "keyframeAnimator" modifier. Outside of the modifier, you can use the "KeyframeTimeline" type to capture a set of keyframes and tracks. You initialize this type with an initial value, and the keyframe tracks that define your animation, just like with the view modifier.
KeyframeTimeline provides API that gives you the duration, which is equal to the duration of the longest track.
And you can calculate values for any time within the range of the animation.
This makes it easy to visualize keyframes with Swift Charts, which I used for the curve visualizations that I showed earlier. This also means that you can use keyframe-defined curves however you want, or to creatively combine keyframes with other APIs, for example, with a geometry proxy to scrub keyframe-driven effects using scroll position, or with a "TimelineView" to update based on time.
And if you’re not sure when you would use this, that’s ok, it’s an advanced tool, and most developers will want to stick to the view modifier.
But it’s here as a building block, and I’m excited to see what creative ways you find to integrate it into your app. That completes our journey.
I hope you’re excited to use this new family of API.
Remember: use phases for chained animations.
They use all of the existing animation types that you already know, so you can get up and running quickly.
Use keyframes for more complex animations where you need complete control.
And finally: have fun exploring.
The world of animation is exciting, and I hope these new tools lead you, and your app, somewhere new.
Thanks! ♪ ♪
-
-
0:42 - Scale Animation
struct Avatar: View { var petImage: Image @State private var selected: Bool = false var body: some View { petImage .scaleEffect(selected ? 1.5 : 1.0) .onTapGesture { withAnimation { selected.toggle() } } } }
-
3:13 - Boolean Phases
OverdueReminderView() .phaseAnimator([false, true]) { content, value in content .foregroundStyle(value ? .red : .primary) } animation: { _ in .easeInOut(duration: 1.0) }
-
6:20 - Custom Phases
ReactionView() .phaseAnimator( Phase.allCases, trigger: reactionCount ) { content, phase in content .scaleEffect(phase.scale) .offset(y: phase.verticalOffset) } animation: { phase in switch phase { case .initial: .smooth case .move: .easeInOut(duration: 0.3) case .scale: .spring( duration: 0.3, bounce: 0.7) } } enum Phase: CaseIterable { case initial case move case scale var verticalOffset: Double { switch self { case .initial: 0 case .move, .scale: -64 } } var scale: Double { switch self { case .initial: 1.0 case .move: 1.1 case .scale: 1.8 } } }
-
9:48 - Keyframes
ReactionView() .keyframeAnimator(initialValue: AnimationValues()) { content, value in content .foregroundStyle(.red) .rotationEffect(value.angle) .scaleEffect(value.scale) .scaleEffect(y: value.verticalStretch) .offset(y: value.verticalTranslation) } keyframes: { _ in KeyframeTrack(\.angle) { CubicKeyframe(.zero, duration: 0.58) CubicKeyframe(.degrees(16), duration: 0.125) CubicKeyframe(.degrees(-16), duration: 0.125) CubicKeyframe(.degrees(16), duration: 0.125) CubicKeyframe(.zero, duration: 0.125) } KeyframeTrack(\.verticalStretch) { CubicKeyframe(1.0, duration: 0.1) CubicKeyframe(0.6, duration: 0.15) CubicKeyframe(1.5, duration: 0.1) CubicKeyframe(1.05, duration: 0.15) CubicKeyframe(1.0, duration: 0.88) CubicKeyframe(0.8, duration: 0.1) CubicKeyframe(1.04, duration: 0.4) CubicKeyframe(1.0, duration: 0.22) } KeyframeTrack(\.scale) { LinearKeyframe(1.0, duration: 0.36) SpringKeyframe(1.5, duration: 0.8, spring: .bouncy) SpringKeyframe(1.0, spring: .bouncy) } KeyframeTrack(\.verticalTranslation) { LinearKeyframe(0.0, duration: 0.1) SpringKeyframe(20.0, duration: 0.15, spring: .bouncy) SpringKeyframe(-60.0, duration: 1.0, spring: .bouncy) SpringKeyframe(0.0, spring: .bouncy) } } struct AnimationValues { var scale = 1.0 var verticalStretch = 1.0 var verticalTranslation = 0.0 var angle = Angle.zero }
-
15:22 - Map Keyframes
struct RaceMap: View { let route: Route @State private var trigger = false var body: some View { Map(initialPosition: .rect(route.rect)) { MapPolyline(coordinates: route.coordinates) .stroke(.orange, lineWidth: 4.0) Marker("Start", coordinate: route.start) .tint(.green) Marker("End", coordinate: route.end) .tint(.red) } .toolbar { Button("Tour") { trigger.toggle() } } .mapCameraKeyframeAnimation(trigger: playTrigger) { initialCamera in KeyframeTrack(\MapCamera.centerCoordinate) { let points = route.points for point in points { CubicKeyframe(point.coordinate, duration: 16.0 / Double(points.count)) } CubicKeyframe(initialCamera.centerCoordinate, duration: 4.0) } KeyframeTrack(\.heading) { CubicKeyframe(heading(from: route.start.coordinate, to: route.end.coordinate), duration: 6.0) CubicKeyframe(heading(from: route.end.coordinate, to: route.end.coordinate), duration: 8.0) CubicKeyframe(initialCamera.heading, duration: 6.0) } KeyframeTrack(\.distance) { CubicKeyframe(24000, duration: 4) CubicKeyframe(18000, duration: 12) CubicKeyframe(initialCamera.distance, duration: 4) } } } }
-
16:26 - KeyframeTimeline
// Keyframes let myKeyframes = KeyframeTimeline(initialValue: CGPoint.zero) { KeyframeTrack(\.x) {...} KeyframeTrack(\.y) {...} } // Duration in seconds let duration: TimeInterval = myKeyframes.duration // Value for time let value = myKeyframes.value(time: 1.2)
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.