1.
Introduction to Animations with SwiftUI
Written by Marin Todorov
SwiftUI is the new paradigm from Apple for building your app UI declaratively. It’s a big departure from the existing UIKit and AppKit frameworks.
When using UIKit and AppKit, you need to constantly micro-manage your data model and your views to keep them in sync. That’s exactly what dictates the need to use a view controller in the first place – you need that class to be the “glue” between the state of your views (aka, what the user sees onscreen) and the state of your data (aka, what’s on disk or in memory).
And of course, you need to monitor user input, like touches or other gestures, and react to those, too. You end up in a never-ending cycle of updating your model and updating the user interface via a multitude of asynchronous callbacks. A lot of this code is very repetitive and prone to errors.
When using SwiftUI, on the other hand, you need to adopt a new approach towards building user interfaces. And let me put you at ease – that new approach is much better than what I just described. In this brave new world, you do not explicitly create views and add them on your screen; instead, you create a view model that SwiftUI renders for you automatically.
Since SwiftUI derives the rendered user interface from your data, you can never have an inconsistent state onscreen – the only source of truth is your data. What’s rendered onscreen is simply a function of that data, so you don’t need to sync states back and forth.
Long story short – you update your data and SwiftUI makes sure that everything looks perfect onscreen.
I realize that if you have solid experience with UIKit or AppKit, SwiftUI might sound like magic, but I can assure you there’s nothing magical about it. It’s a rock-solid, modern-day UI framework.
If you want to learn SwiftUI in depth, check out the “SwiftUI by Tutorials” book from raywenderlich.com at:
Note: If you’d like to make the most out of the frameworks that Apple introduced in 2019, I recommend using Combine alongside SwiftUI. You can learn about Combine in detail in “Combine: Asynchronous programming with Swift”:
In this book, you are going to focus on animations only. Therefore, getting some understanding of SwiftUI before you get to work on the next two chapters would be beneficial. However, even if you don’t have any experience with SwiftUI, you can still work through the chapters and create some beautiful animations.
It’s time to get started!
SwiftUI basics
As you established in the previous section, when using SwiftUI, you describe your user interface declaratively and leave the rendering to the framework.
Each of the views you declare for your UI (those can be anything, like text labels, images or shapes) adheres to the View
protocol. View
requires each view struct to feature a property called body
.
Any time you change your data model, SwiftUI
asks each of your views for their current body
, which might change according to changes in your latest data model. It then builds the view hierarchy to render onscreen. In a sense, SwiftUI
makes “snapshots” triggered by changes in the data model.
Since SwiftUI
can identify all views in your view hierarchy, it’s not difficult for it to animate any changes to your views. If in the current snapshot of your views, a given view is 100 pixels to the right, it’s a piece of cake for SwiftUI
to animate this change.
It works the same way if your view is larger than it used to be or if it’s now purple where it used to be pink. SwiftUI
keeps track of the state in the previous snapshot and, if you need it to, can animate any changes you declare for your views.
Doesn’t that sound just amazing?
Having covered the very basics of SwiftUI, try some animations in Xcode and see how it works in practice.
Your first SwiftUI animation
To get started, open the starter project provided for this chapter of the book. The project consists of two parts – a simple playground view, where you’ll try some basic animations, and a more complete example called Savanna Tours, where you’re going to try some real-life UI animations.
SwiftUI
introduces some new tools in Xcode. To be able to work through the next two chapters, you’ll need a basic understanding of these new tools.
Xcode 11 introduces a new Editor mode called Canvas. A Canvas editor displays your code visually and in real-time and allows you to re-run your code very quickly.
The basic controls that you’ll need to master for this chapter are as follows:
- Adjust Editor Options: A drop-down menu that allows you to enable the new live-preview Xcode editor called Canvas.
- The Canvas Run button: Allows you to switch between static and interactive previews of your code.
- The Canvas zoom: Helps you zoom out the preview and see your view layout in full, or to zoom into particular view you want to see in detail.
To get into the SwiftUI
workflow, select StarterView.swift in the Project Navigator and make sure you see the Editor Canvas. To do that, click Adjust Editor Options in the drop-down menu and click Editor and Canvas from the list of options:
Now, you should see a split editor with StarterView.swift on the left-hand side and a nice green circle on the right-hand side. That circle is the preview of your layout.
It’s important to notice that Xcode currently displays a static preview of the layout. You cannot interact with the preview and, most importantly for you, you will not see any animations in action. The static preview makes an initial snapshot of your layout and displays that onscreen.
To switch to interactive previewing, click the Canvas Run button. That will make Xcode compile StarterView
and run an interactive preview. To give you a visual hint that you are now running your code interactively, Xcode changes the background color of the Canvas and switches the play icon on the Canvas Run button to a stop icon. Clicking the button again will switch back to static previewing.
For the next two chapters, you’ll need to enable interactive previewing, since you will be playing with animations. Speaking of which, it’s finally time to try some code!
Look at the code of the body
property inside StarterView
– this is where you declare your UI, which is currently a simple green circle:
Circle()
.scaleEffect(0.5)
.foregroundColor(Color.green)
Next, extract the circle foreground color into a state property and try to animate it. Add a new property to StarterView
called color
:
@State var color = Color.green
Marking properties as @State
will trigger a new snapshot of your view each time they’re modified. To update your user interface, you don’t adjust your views directly; instead, you modify state properties. That triggers a new render of your layout automatically for you.
Next, use the new state property instead of the fixed foreground color. Replace the foregroundColor(...)
modifier on your circle with:
.foregroundColor(color)
Now, each time you modify color
, SwiftUI will create a new snapshot of the circle with its new foreground tint. To see that in action, add a little timeout after adding the circle onscreen and change the color
property.
Inside onAppear
, insert the code that changes the circle color so that it looks like this:
.onAppear {
delay(seconds: 2) {
self.color = .red
}
}
delay(seconds:block:)
is a helper function declared in the starter project that executes a closure after a given amount of seconds. In the case above, you say that 2
seconds after the circle appears onscreen, you’d like to change the color
state property’s value to red.
As you keep adding code throughout the chapter, you could confuse the Xcode build system, as it will eagerly try to show your changes onscreen and in real time. If it gets too confused, Xcode will pause the previewing and you might see a banner like this on top of the Canvas:
In that case, just press Try again or Resume to get the preview running again. You could also press Diagnostics to get additional information about why the preview got stuck. Oftentimes, it’s an issue with your code like a failed type inference or something along those lines.
Once the interactive preview is running in the Canvas editor (make sure you’ve clicked the Canvas Run button), you’ll see the green circle turn into a red circle after a small delay:
Note: Congrats, you’ve just accidentally drawn the Japanese flag!
Next, you need to add another modifier on your circle view to let know SwiftUI that you’d like it to animate the layout changes.
Append the following line just after the line where you call foregroundColor(_)
:
.animation(.default)
animation(_)
sets the default animation to apply to any layout changes for the given view. In this case, SwiftUI will use a default animation with pre-defined parameters for any changes on that view.
As soon as you save your code changes, Xcode re-runs the interactive preview. You should see the circle color animate with a nice crossfade transition from green to red:
Congratulations, you’ve created your first SwiftUI animation!
The Animation type
You easily created an animation by adding the animation(_)
modifier, but what is the .default
value that you passed as the parameter?
animation(_)
takes an Animation
as a parameter, which defines the properties of the resulting animation effect. These properties include the duration (how long the animation takes), the timing curve (you’ll learn more about timing curves in Chapter 3, “Getting Started with View Animations”) or whether it should be a spring-driven animation (you’ll cover springs in detail in Chapter 4, “Springs”).
You create an Animation
using one of the following static methods:
-
Animation.easeIn(duration:): A method that produces a basic animation with ease-in timing curve, and the given duration. There are variants for the other preset timing curves:
.easeOut
,.easeInOut
andlinear
. Omitting the duration gives you a default duration of 0.35 seconds..default
is a basic animation with 0.35 seconds duration and an ease-in-ease-out timing curve. -
Animation.interpolatingSpring(mass:stiffness:damping:initialVelocity): This method creates a spring-driven animation which interpolates between the previous and current states of your view, but depending on its parameters, could overshoot its final value to produce “bouncy” animations. You can learn much more about spring-driven animations in chapters 4 and 13.
-
Animation.spring(response:dampingFraction:blendDuration:): A method that creates a different type of spring-driven animations which have a different configuration that allows you to create more “fluid” effects.
Once you create your animation instance, you can use the following modifiers to change the animation even more:
- delay(_): Adds a delay of a specific amount of time before the animation starts.
-
repeatCount(_:autoreverses:): Configures the animation to repeat a given number of times. If
autoreverses
is set, it plays the effect forwards and backwards instead of restarting the animation on each repeat. - repeatsForever(autoreverses:): Like above, this modifier makes the animation repeat, but it repeats indefinitely instead of a given number of times.
- speed(_): Allows you to speed up or slow down the animation.
This sounds amazing, especially if you’ve created animations before with other frameworks. But with SwiftUI, custom animations for views, controls and shapes is really easy. You just have to create an animation instance by choosing one of the the factory methods, applying any of the few modifiers you just learned about and adding the animation to a view by using the animation(_)
modifier!
You’ll create a few more animations throughout this chapter and the next one, so you’ll see there’s no other secret sauce to SwiftUI’s animations. It’s a real joy to quickly iterate and experiment with your layout and UI effects.
Next – back to finishing that first SwiftUI animation you had going earlier. You’re going to quickly explore a couple more ideas and then move on to building a full-blown real-life app animation.
As you saw, creating animations is just a matter of changing properties that you’ve declared as your view’s state. SwiftUI automatically knows how to detect the differences between the current and previous snapshots of your UI and how to apply animations to the views featuring the animation(_)
modifier.
The starter code in StarterView
includes a list of colors that you can easily cycle through. Using it makes for a more interesting animation.
Delete the code inside onAppear { ... }
and replace it with:
for index in 1..<offsets.count {
delay(seconds: Double(index)) {
self.currentOffset = index
}
}
This schedules a number of changes to currentOffset
over time, like so:
Now instead of using the color
property for your circle you’ll use the values predefined in colors
. To do that, find foregroundColor(_)
and replace it with:
.foregroundColor(colors[currentOffset])
When you save, Xcode automatically recompiles your code and you’ll observe the circle crossfade into a number of colors before ending on its initial green shade.
As your last step in building your first SwiftUI animation, you’ll send your circle view on a trip around the … screen!
Views in SwiftUI have a modifier called offset(x:y:)
. You’ll use this modifier to feed your circle different offsets over time and make it move around.
The starter code already includes an array of offsets called offsets
and you’ve already set your currentOffset
state property. So the only thing that’s left to do is to add the offset
modifier and be on your way.
Add this on the line below the animation(_)
modifier:
.offset(x: offsets[currentOffset].x,
y: offsets[currentOffset].y)
Each time you modify currentOffset
, SwiftUI grabs the current offset from offsets
and animates the circle to its next location onscreen. Check the preview and you’ll see your circle animate through both colors and offsets:
With that, your first SwiftUI animation is complete. Time to take on the world! I mean – time to play with a real-life animation and learn about animating even more SwiftUI modifiers.
Exploring more view modifiers
For this section of the chapter, you’ll work on a project called Savanna Tours. Click ContentView.swift in the Project Navigator to open that file and you’ll see the initial layout of that view in the Canvas on the right side of the split:
ContentView
is part of an app that offers tours in the African savanna. In the Canvas Editor, you can see each tour’s details including a title, a short description, a large photo, and a list of the tour’s milestones.
Click the Canvas Run button to run an interactive preview and you’ll see the only interactivity available at the moment is scrolling through the tour’s milestones.
In this section of the chapter, you’ll make the tour thumbnail, which you can find on the right side of the tour’s title, interactive. Your app’s users will be able to tap on the thumb and see a larger version of it onscreen presented, and dismissed, with a neat animation.
First of all, just like in the previous example, you’ll need a property to hold some of your state. Namely, whether the thumbnail is zoomed or not. A simple Boolean state property suffices for that purpose, so add this to ContentView
:
@State var zoomed = false
To toggle between zoomed-in and -out states, add onTapGesture {...}
to the thumbnail. Add this code directly below .shadow(radius: 10)
:
.onTapGesture {
self.zoomed.toggle()
}
Each time you tap on the thumbnail view, the zoomed state will alternate between false
and true
.
Now, put that new state to work. Find the current scaleEffect(_)
and change it to:
.scaleEffect(self.zoomed ? 1.33 : 0.33)
With this modifier, each time you tap on the thumb it will alternate between 33%
of its original size and 133%
of its original size.
Go ahead and click the thumb in the preview… oh, no! What happened? The thumb disappears when you click it for the first time.
This happens because the hard-coded position of (600, 50)
works only when the thumbnail is scaled down to its 33%
size. Once the image is in full size, it repositions out of the screen.
Go ahead and patch this for now; you’ll find a better solution later in the chapter. Replace the current position(x:y:)
with:
.position(x: self.zoomed ? 220 : 600, y: 50)
Now, click through the zoomed-in and zoomed-out states and you should see that it works just fine now. Although you may have noticed that the zoomed-in version is not centered onscreen. That’s a bit annoying, but you’ll fix that later on.
The last step to get your animation going is, just like in the previous section, adding animation(_)
on the view you’d like to animate.
Right below .shadow(radius: 10)
add:
.animation(.default)
Click a few times on the thumbnail and observe the animation. Notice how SwiftUI automatically interpolates the multiple animations you’re running on the same thumb view:
- The first animation scales the thumb way up, which pushes its location to the right. If you weren’t changing its position as well, it would’ve animated out of the screen.
- The second animation animates the thumb’s position leftwards to keep it inside the screen while it scales up.
SwiftUI interpolates the changes of both animations. The final result is a neatly curving motion that starts rightwards but turns around and ends to the left of its starting point. That builds a very nice zoom effect!
To make that zoom even more slick, replace the default animation with a spring-driven animation:
.animation(.spring())
The default spring animation uses a harmonic oscillator under the hood with some default presets to deliver a very nice timing curve for your animations. You’ll learn more about the mechanics of spring animations later in this book.
Click the thumbnail again and enjoy the new effect. Click repeatedly in quick succession and you’ll notice interrupting the current animation never gives you a “broken” layout. All animations in SwiftUI are interruptible and reversible by default, and you get this for free out of the box.
Next, you’re going to add another animation to yet another view on your screen. This way, you’ll see how to drive multiple animations onscreen from a single state change.
In the code, scroll up to find the TourTitle
view, which displays the tour’s title and description onscreen. Add the following right after .padding(.leading, 30)
to animate that view:
.offset(x: self.zoomed ? 500 : 0, y: -15)
.animation(.default)
The code is similar to what you’ve done before. You add animation(_)
to the view and you change the value of offset(x:y:)
based on your current state.
Clicking the thumb will now hide the tour title under the scaling-up thumb, resulting in a nice transition back and forth from the zoomed state.
Go back to the thumb view next and experiment with more modifiers. Right below the line Image("thumb")
(the order of modifiers matters), add:
.clipShape(RoundedRectangle(cornerRadius: self.zoomed ? 40 : 500 ))
This will clip the thumbnail to the shape you give as a parameter:
Since the corner radius of the clip rectangle is bigger than half its side’s length, SwiftUI rounds the rectangle to a circle. Now, the thumbnail looks really nice! Even better, when you click the thumb, it will animate its clipping shape to a rounded rectangle with a smaller corner radius and give the thumb a completely different shape:
And of course, all the changes to the thumb’s layout are nicely animated with a fluid spring animation.
SwiftUI makes drawing and animating shapes really easy, because you apply the same basic principles to both views and shapes. You don’t need to differentiate between them and they’re both first-class citizens of your UI.
You’ll learn much more about working with shapes in the next chapter, but first, add one more animation in your current project to add even more glow to the thumbnail view.
Below your clipShape(_)
modifier insert:
.overlay(
Circle()
.fill(self.zoomed ? Color.clear : Color(red: 1.0, green: 1.0, blue: 1.0, opacity: 0.4))
.scaleEffect(0.8)
)
This code is somewhat meta, so it might take a while to grasp. You’re using overlay(_)
on your thumb view, which adds another view as an overlay over your original view.
overlay(_)
takes yet another view as its parameter; you just create a Circle
inline and set its modifiers, then pass it onto overlay(_)
. The circle overlay is 80%
of the size of its parent (the thumb) and it’s filled with a semi-transparent white color. This makes it look more like a nice badge than a flat thumbnail:
When you zoom the thumbnail, the circular overlay will disappear as you animate its white fill to a transparent clear
color.
Now, add one more modifier directly after the one you just added:
.saturation(self.zoomed ? 1 : 0)
saturation(_)
modifies the color saturation of the current view. The code you just added will desaturate the thumb completely when it’s small and will animate it to full-color beauty when it zooms in.
Aaaand congratulations – you’ve built a beautiful animation by using a single state property and by leveraging SwiftUI’s animation super-powers!
In this chapter, you learned a bit about SwiftUI and building user interfaces declaratively. While this book focuses specifically on animations, you saw that SwiftUI is a modern, declarative and, let’s be honest, an easy-to-use UI framework.
Besides being able to quickly iterate and build your UI, creating eye-stunning animations with SwiftUI is pure joy!
This chapter includes a couple of quick exercises to iterate over your newly acquired skills; you’ll learn more about creating more complex animations in the next chapter.
Key points
- SwiftUI lets you build your UI declaratively and ask it to animate any changes between “snapshots” of your view hierarchy.
- You add animations via the
.animation(_)
modifier on your views. You can provide a default or custom animation as the parameter. - You animate various view modifiers via changes to the view’s state properties, which trigger a new snapshot and which ask SwiftUI to interpolate between the previous and current layout.
Challenges
Challenge 1: Adding a rotation animation
To try one more type of animation, try adding a rotationEffect(_)
to your thumb. Alternate between 0
and 90
degree angles for the rotation, depending on whether the thumb is zoomed in or not. If you’d like a tip – insert the rotation modifier before you adjust the position and scale of the thumb.
The initial state of the UI should look like this:
Challenge 2: Center the zoomed thumbnail
In this challenge, you’ll learn how to animate views’ positions onscreen based not on a relative offset, but rather on its relative position to the screen size. This will finally let you center that thumbnail horizontally on the screen.
To access the geometry (or the position and size) of a parent view in SwiftUI, use GeometryReader
to access the thumb’s container view width and to animate the thumb right in its horizontal center when scaled up.
To do that, wrap Image("thumb")
and all its modifiers inside a block like this:
GeometryReader() { geometry in
< your existing code here >
}
Inside the closure, you have access to an object called geometry
, which gives you the size and position of the thumb’s parent view.
Now you can replace the original thumbnail position(x:y:)
with:
.position(x: self.zoomed ? geo.frame(in: .local).midX : 600,
y: 60)
You keep the thumbnail at an x position of 600
, but when scaled up, you move it to the midX
coordinate of its parent frame.
This way, when you click the thumb one last time, you’ll see it neatly centered onscreen:
And that’s a wrap! Get ready for the next chapter, which will lead you through building animation effects that will simply make your head spin…