
12 Minutes
Creating Smooth and Engaging UI with SwiftUI Animations
Fix Bugs Faster! Log Collection Made Easy
Animations make our apps seem more alive, approachable and interactive, and they add that extra level of polish, which is crucial in an age of ever-greater consumer choice.
One of the great things about Swift is that it provides a range of animation functionality straight out of the box. In this post, we’ll explore some of this functionality and demonstrate some popular techniques for SwiftUI animations, so you can use them on your own projects.
This article will be useful for
- Basic iOS app developers who want to see what can be done using SwiftUI.
- Advanced devs who have already started working with SwiftUI animations and want to dig deeper.
Ready? Let’s get started.
Table of Contents [show]
Basic Animations
The most basic SwiftUI animations we can make consist of animating view properties, which will change the core look and feel of our visual displays.
There are several animatable component properties that can be adjusted through a SwiftUI animation, and here are a few:
- Position and Frame:
.offset(x: isMoved ? 100 : 0)
- Opacity:
.opacity(isVisible ? 1 : 0)
- Rotation:
.rotationEffect(.degrees(isRotated ? 360 : 0))
- Scale:
.scaleEffect(isScaled ? 2 : 1)
- Color:
.foregroundColor(isActive ? .green : .red)
Now that we’ve seen those basic animation properties, let’s look at how to use them with an example.
We’ll use two buttons, one to show/hide a label, and another one to move it around. Our code looks like the following:
import SwiftUI
struct ContentView: View {
@State private var isVisible: Bool = true
@State private var isMoved: Bool = false
var body: some View {
VStack {
Button {
withAnimation {
isVisible = !isVisible
}
} label: {
Text("Animate show/hide")
}
Button {
withAnimation {
isMoved = !isMoved
}
} label: {
Text("Move")
}
Text("Animating text")
.opacity(isVisible ? 1 : 0)
.offset(x: isMoved ? 100 : 0)
}
.padding()
}
}
#Preview {
ContentView()
}
Now let’s break it Down:
- State Management: The
@State
propertiesisVisible
andisMoved
control the visibility and position of the text we’re animating. - withAnimation block: The
.withAnimation
mark this animation as an Explicit animation. We will further define explicit and implicit animations in due course. - Interactive Trigger: There are two
Buttons
that toggle our@state
properties, and make our animation happen.
Animation Types in SwiftUI
SwiftUI provides a default range of built-in animation types that are quite easy to use:
1. Timing Functions
- Ease-In: Starts slowly and accelerates.
.animation(.easeIn(duration: 1.0))
- Ease-Out: Starts quickly and slows down.
.animation(.easeOut(duration: 1.0))
- Ease-In-Out: Smooth acceleration and deceleration.
.animation(.easeInOut)
- Linear: Constant speed throughout, useful if you want something smooth and consistent.
.animation(.linear(duration: 1.0))
2. Spring Animations
Spring animations simulate physical motion, adding a natural feeling to our UIs.
.animation(.spring(response: 0.5, dampingFraction: 0.7))
- Response: Determines the animation duration.
- Damping Fraction: Controls how oscillations change over time.
3. Custom Timing Curves
Now, let’s start exploring animation modifier techniques for advanced use cases.
We can define our own timing curves using TimingCurve
to create a custom animation. This is however a lot more complex, and it relies on 4 points and a duration:
x0, y0, x1, y1, being that x0 and y0 are the initial horizontal and vertical control points, and that x1 and y1 are the final control points. All their values are between 0 and 1.
.animation(.timingCurve(x0, x1, y0, y1, duration: 1.0))
//as an example:
.animation(.timingCurve(0.25, 0.1, 0.25, 1, duration: 1.0))
Implicit Animation vs Explicit Animation
SwiftUI supports two styles of animations: implicit and explicit.
Implicit Animations
With an implicit animation, you define the animation directly on the view with .animation()
. This triggers an animation whenever a bound state changes.
Text("Hello, SwiftUI!")
.opacity(isVisible ? 1 : 0)
.animation(.easeInOut, value: isVisible)
Implicit animations are great for simple state-driven transitions.
Explicit Animations
For more control, you can use explicit animations with the withAnimation
function. This lets you wrap specific actions in an animation block. We’ve seen this earlier in the basic animations chapter, and if you go back and look at it, you’ll see the explicit animations at work. We’ll reuse the same example as before:
import SwiftUI
struct ContentView: View {
@State private var isVisible: Bool = true
@State private var isMoved: Bool = false
var body: some View {
VStack {
Button {
withAnimation {
isVisible = !isVisible
}
} label: {
Text("Animate show/hide")
}
Button {
withAnimation {
isMoved = !isMoved
}
} label: {
Text("Move")
}
Text("Animating text")
.opacity(isVisible ? 1 : 0)
.offset(x: isMoved ? 100 : 0)
}
.padding()
}
}
#Preview {
ContentView()
}
Transitions
Now let’s dig a little deeper.
We use transitions to create a smooth migration between two screens. We use them to tell the user all kinds of things, for example that the page is loading or that the app is working on their request, and they’re great for breaking down information into bite-sized chunks across several screens (a process known as progressive disclosure).
To make the transitions, we apply animations when views are added or removed from a view hierarchy. You can look at them slightly differently from the animations we’ve seen so far. Instead of “how can I make this view animate?” we may consider “How do I want this view to initially show/hide?”
Built-In Transitions
SwiftUI includes several built-in transitions that we can use in our apps:
.opacity
.scale
.slide
.move(edge: .leading)
Here’s an example:
struct TransitionView: View {
@State private var showDetails = false
var body: some View {
VStack {
if showDetails {
Text("Details")
.transition(.slide)
}
Button("Toggle Details") {
withAnimation {
showDetails.toggle()
}
}
}
}
}
Combining Transitions
This is where the real magic happens. We can string together multiple transitions to create a storybook experience for our users.
It’s really easy, too. All we need to do is use .combined
.
.transition(.scale.combined(with: .opacity)
Custom Transitions
Create custom transitions using AnyTransition
.
extension AnyTransition {
static var customFadeAndMove: AnyTransition {
AnyTransition.opacity.combined(with: .move(edge: .bottom))
}
}
Advanced Animations
Animation Chains
Now we’re really at the sharp end. We’ve mastered the basics of animations,, and now we’re using them to create a really sharp, sophisticated UI.
To further enhance that storybook feel we mentioned earlier, we can chain multiple animations together and create complex sequences. Again, it’s relatively simple provided we use withAnimation
:
withAnimation(.easeIn(duration: 0.5)) {
isVisible.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(.easeOut(duration: 0.5)) {
isScaled.toggle()
}
}
Animating Paths
If you’re an experienced Swift developer, you’ve probably used Paths already. But in case not, they’re seriously useful, enabling us to create any kind of custom shape we want.
To animate hapes and paths, we can leverage the Animatable
protocol and AnimatablePair
. Here’s an example:
struct AnimatingShape: Shape {
var animatableData: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY),
radius: animatableData,
startAngle: .zero,
endAngle: .degrees(360),
clockwise: true)
return path
}
}
Animations and State
SwiftUI animations are tightly coupled with our app’s state. And the good news is that we can use @State
, @Binding
, or @ObservedObject
to trigger animations.
Here’s an example using ObservedObject. Remember, it’s wise to ensure that state changes are minimal to prevent unintended side effects (we’ll cover some more best practices in the following section):
class AnimationModel: ObservableObject {
@Published var isAnimating = false
}
struct ObservedAnimationView: View {
@ObservedObject var model = AnimationModel()
var body: some View {
Circle()
.scaleEffect(model.isAnimating ? 2 : 1)
.animation(.spring(), value: model.isAnimating)
.onTapGesture {
model.isAnimating.toggle()
}
}
}
Best Practices
We’ve gone through lots of different animation code strings today, but remember: there are certain best practices that underpin all of them. Here are four tips that should form part of your animation toolkit.
- Test Performance: Complex animations may impact performance, especially on older devices.
- Use Timing Wisely: Select appropriate easing curves and durations to make animations feel natural.
- Combine Carefully: Avoid overly complex transitions to maintain clarity.
- Provide User Feedback: Use animations to guide users without overwhelming them.
To Sum Up
SwiftUI simplifies the process of adding animations to our apps, allowing us to create immersive experiences with minimal effort. By leveraging implicit and explicit animations, transitions, and advanced techniques, we can design UIs that are not just functional but delightful.
Mastering animations in SwiftUI will take your app development skills to the next level, enabling you to build polished, professional-grade applications. We hope we’ve given you the building-blocks today, but if you have any further questions, don’t hesitate to reach out.
Happy coding!
Expect The Unexpected!
Debug Faster With Bugfender