SwiftUI Animations - Bring Your UI to Life
Animations are what make your app feel alive and professional. SwiftUI makes animations incredibly easy with built-in support for smooth, performant transitions.
What are SwiftUI Animations?
SwiftUI animations automatically interpolate between view states. When a property changes, SwiftUI can animate that change smoothly instead of jumping instantly to the new value.
Why Animations Matter
📱 User Experience
- Guide user attention
- Provide feedback for actions
- Make interfaces feel responsive
✨ Professional Polish
- Apps feel more premium
- Smooth transitions reduce cognitive load
- Delightful micro-interactions
Implicit Animations
The simplest way to add animations - apply .animation() modifier to views.
Basic Implicit Animation
struct ImplicitAnimationExample: View { @State private var isExpanded = false
var body: some View { VStack { RoundedRectangle(cornerRadius: isExpanded ? 50 : 10) .fill(isExpanded ? Color.blue : Color.red) .frame(width: isExpanded ? 300 : 100, height: isExpanded ? 300 : 100) .animation(.default, value: isExpanded)
Button("Toggle") { isExpanded.toggle() } .padding() } }}Key Points:
.animation(.default, value: isExpanded)animates changes toisExpanded- All animatable properties change smoothly
- SwiftUI handles the interpolation
Animation Curves
Different timing curves for different effects:
struct AnimationCurvesExample: View { @State private var offset: CGFloat = 0
var body: some View { VStack(spacing: 30) { // Linear - constant speed Circle() .fill(Color.red) .frame(width: 50, height: 50) .offset(x: offset) .animation(.linear(duration: 1), value: offset)
// Ease In - starts slow Circle() .fill(Color.blue) .frame(width: 50, height: 50) .offset(x: offset) .animation(.easeIn(duration: 1), value: offset)
// Ease Out - ends slow Circle() .fill(Color.green) .frame(width: 50, height: 50) .offset(x: offset) .animation(.easeOut(duration: 1), value: offset)
// Ease In Out - slow at both ends Circle() .fill(Color.orange) .frame(width: 50, height: 50) .offset(x: offset) .animation(.easeInOut(duration: 1), value: offset)
Button("Animate") { offset = offset == 0 ? 150 : 0 } } }}Explicit Animations
More control using withAnimation - animates all changes within the closure.
Basic Explicit Animation
struct ExplicitAnimationExample: View { @State private var rotation: Double = 0 @State private var scale: CGFloat = 1
var body: some View { VStack { Image(systemName: "star.fill") .font(.system(size: 100)) .foregroundColor(.yellow) .rotationEffect(.degrees(rotation)) .scaleEffect(scale)
Button("Animate") { withAnimation(.spring(response: 0.5, dampingFraction: 0.6)) { rotation += 360 scale = scale == 1 ? 1.5 : 1 } } } }}Advantages:
- Animate multiple properties together
- Fine control over timing
- Cleaner for complex animations
Different Animation Types
struct AnimationTypesExample: View { @State private var isAnimated = false
var body: some View { VStack(spacing: 40) { // Default animation Button("Default") { withAnimation { isAnimated.toggle() } }
// Linear animation Button("Linear (2s)") { withAnimation(.linear(duration: 2)) { isAnimated.toggle() } }
// Spring animation Button("Spring") { withAnimation(.spring()) { isAnimated.toggle() } }
// Custom spring Button("Bouncy Spring") { withAnimation(.spring(response: 0.3, dampingFraction: 0.3)) { isAnimated.toggle() } }
// Animated view Circle() .fill(isAnimated ? Color.blue : Color.red) .frame(width: isAnimated ? 200 : 100, height: isAnimated ? 200 : 100) } }}Spring Animations
Spring animations feel natural and responsive - like a real spring!
Spring Parameters
struct SpringAnimationExample: View { @State private var position: CGFloat = 0
var body: some View { VStack(spacing: 30) { // Stiff spring (quick, minimal bounce) Circle() .fill(Color.red) .frame(width: 50, height: 50) .offset(x: position) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: position)
// Medium spring (balanced) Circle() .fill(Color.blue) .frame(width: 50, height: 50) .offset(x: position) .animation(.spring(response: 0.5, dampingFraction: 0.6), value: position)
// Bouncy spring (slow, lots of bounce) Circle() .fill(Color.green) .frame(width: 50, height: 50) .offset(x: position) .animation(.spring(response: 0.8, dampingFraction: 0.3), value: position)
HStack { Button("Left") { position = -100 }
Button("Center") { position = 0 }
Button("Right") { position = 100 } } } }}Spring Parameters:
- response - Duration of spring animation (higher = slower)
- dampingFraction - How much bounce (0 = infinite bounce, 1 = no bounce)
- blendDuration - How animations blend together
Transitions
Transitions define how views appear and disappear.
Built-in Transitions
struct TransitionsExample: View { @State private var showView = false @State private var transitionType = 0
var body: some View { VStack(spacing: 20) { Picker("Transition", selection: $transitionType) { Text("Opacity").tag(0) Text("Scale").tag(1) Text("Slide").tag(2) Text("Move").tag(3) } .pickerStyle(.segmented)
Spacer()
if showView { RoundedRectangle(cornerRadius: 20) .fill(Color.blue) .frame(width: 200, height: 200) .transition(selectedTransition) }
Spacer()
Button(showView ? "Hide" : "Show") { withAnimation(.spring()) { showView.toggle() } } } .padding() }
var selectedTransition: AnyTransition { switch transitionType { case 0: return .opacity case 1: return .scale case 2: return .slide case 3: return .move(edge: .bottom) default: return .opacity } }}Combined Transitions
Combine multiple transitions:
struct CombinedTransitionsExample: View { @State private var showCard = false
var body: some View { VStack { if showCard { CardView() .transition(.scale.combined(with: .opacity)) }
Button("Toggle Card") { withAnimation(.spring()) { showCard.toggle() } } } }}
struct CardView: View { var body: some View { RoundedRectangle(cornerRadius: 20) .fill( LinearGradient( colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 300, height: 200) .shadow(radius: 10) }}Asymmetric Transitions
Different animations for insertion and removal:
struct AsymmetricTransitionExample: View { @State private var showNotification = false
var body: some View { VStack { Spacer()
if showNotification { NotificationView() .transition( .asymmetric( insertion: .move(edge: .top).combined(with: .opacity), removal: .move(edge: .trailing).combined(with: .opacity) ) ) }
Spacer()
Button("Show Notification") { withAnimation { showNotification = true }
// Auto-hide after 2 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 2) { withAnimation { showNotification = false } } } } }}
struct NotificationView: View { var body: some View { HStack { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) Text("Success!") .fontWeight(.semibold) } .padding() .background(Color.green.opacity(0.2)) .cornerRadius(10) .padding() }}Matched Geometry Effect
Create smooth transitions when views move between positions.
Basic Matched Geometry
struct MatchedGeometryExample: View { @State private var isExpanded = false @Namespace private var animation
var body: some View { VStack { if !isExpanded { CompactView(namespace: animation) .onTapGesture { withAnimation(.spring()) { isExpanded = true } } } else { ExpandedView(namespace: animation) .onTapGesture { withAnimation(.spring()) { isExpanded = false } } } } }}
struct CompactView: View { let namespace: Namespace.ID
var body: some View { HStack { Circle() .fill(Color.blue) .frame(width: 50, height: 50) .matchedGeometryEffect(id: "avatar", in: namespace)
Text("John Doe") .matchedGeometryEffect(id: "name", in: namespace) } .padding() .background(Color.gray.opacity(0.2)) .cornerRadius(10) }}
struct ExpandedView: View { let namespace: Namespace.ID
var body: some View { VStack(spacing: 20) { Circle() .fill(Color.blue) .frame(width: 150, height: 150) .matchedGeometryEffect(id: "avatar", in: namespace)
Text("John Doe") .font(.title) .matchedGeometryEffect(id: "name", in: namespace)
Text("Software Developer") .foregroundColor(.gray) } .frame(maxWidth: .infinity) .padding() .background(Color.white) .cornerRadius(20) .shadow(radius: 10) .padding() }}Animatable Modifiers
Create custom animated effects with AnimatableModifier.
Custom Wave Effect
struct WaveModifier: AnimatableModifier { var waveHeight: Double
var animatableData: Double { get { waveHeight } set { waveHeight = newValue } }
func body(content: Content) -> some View { content .offset(y: sin(waveHeight) * 20) }}
struct CustomAnimationExample: View { @State private var waveOffset = 0.0
var body: some View { VStack { Text("Waving Text!") .font(.largeTitle) .modifier(WaveModifier(waveHeight: waveOffset))
Button("Wave") { withAnimation(.linear(duration: 2).repeatForever(autoreverses: false)) { waveOffset = .pi * 2 } } } }}Repeating Animations
Animations that loop continuously.
Pulsing Effect
struct PulsingExample: View { @State private var isPulsing = false
var body: some View { VStack { Circle() .fill(Color.red) .frame(width: 100, height: 100) .scaleEffect(isPulsing ? 1.3 : 1.0) .opacity(isPulsing ? 0.5 : 1.0) .animation( .easeInOut(duration: 1) .repeatForever(autoreverses: true), value: isPulsing )
Button("Start Pulsing") { isPulsing = true } } .onAppear { isPulsing = true } }}Loading Spinner
struct LoadingSpinner: View { @State private var isRotating = false
var body: some View { Circle() .trim(from: 0, to: 0.7) .stroke(Color.blue, lineWidth: 5) .frame(width: 50, height: 50) .rotationEffect(.degrees(isRotating ? 360 : 0)) .animation( .linear(duration: 1) .repeatForever(autoreverses: false), value: isRotating ) .onAppear { isRotating = true } }}Practical Example - Animated Menu
struct AnimatedMenu: View { @State private var isMenuOpen = false @State private var selectedItem: String?
let menuItems = ["Profile", "Settings", "Help", "Logout"]
var body: some View { ZStack { // Background Color.black.opacity(isMenuOpen ? 0.3 : 0) .ignoresSafeArea() .onTapGesture { withAnimation(.spring()) { isMenuOpen = false } }
VStack { Spacer()
// Menu items if isMenuOpen { VStack(spacing: 15) { ForEach(Array(menuItems.enumerated()), id: \.offset) { index, item in MenuItemButton(title: item) { selectedItem = item withAnimation(.spring()) { isMenuOpen = false } } .transition( .scale.combined(with: .opacity) ) .animation( .spring(response: 0.3, dampingFraction: 0.7) .delay(Double(index) * 0.05), value: isMenuOpen ) } } .padding() .background(Color.white) .cornerRadius(20) .shadow(radius: 20) .padding() .transition(.move(edge: .bottom).combined(with: .opacity)) }
// Floating action button Button { withAnimation(.spring()) { isMenuOpen.toggle() } } label: { Image(systemName: isMenuOpen ? "xmark" : "plus") .font(.title2) .foregroundColor(.white) .frame(width: 60, height: 60) .background(Color.blue) .clipShape(Circle()) .rotationEffect(.degrees(isMenuOpen ? 45 : 0)) .shadow(radius: 10) } .padding() } } }}
struct MenuItemButton: View { let title: String let action: () -> Void
var body: some View { Button(action: action) { HStack { Image(systemName: iconName) .frame(width: 30) Text(title) .fontWeight(.medium) Spacer() } .padding() .background(Color.gray.opacity(0.1)) .cornerRadius(10) } .buttonStyle(.plain) }
var iconName: String { switch title { case "Profile": return "person.fill" case "Settings": return "gear" case "Help": return "questionmark.circle" case "Logout": return "rectangle.portrait.and.arrow.right" default: return "circle" } }}Best Practices
1. Use Value-Based Animations (iOS 17+)
// ✅ Good - Explicit value binding.animation(.spring(), value: isExpanded)
// ⚠️ Be careful - Animates everything.animation(.spring())2. Choose Appropriate Duration
// ✅ Good - Quick micro-interactions.animation(.spring(response: 0.3), value: state)
// ❌ Avoid - Too slow for simple changes.animation(.linear(duration: 5), value: state)3. Spring for Natural Feel
// ✅ Good - Feels responsivewithAnimation(.spring(response: 0.5, dampingFraction: 0.7)) { // changes}
// ⚠️ Linear can feel roboticwithAnimation(.linear) { // changes}4. Don’t Over-Animate
// ✅ Good - Purposeful animationButton("Save") { } .scaleEffect(isSaving ? 0.95 : 1.0) .animation(.spring(), value: isSaving)
// ❌ Avoid - DistractingText("Hello") .animation(.spring().repeatForever(), value: someValue) // Too much!5. Performance Considerations
// ✅ Good - Animate simple properties.opacity(isVisible ? 1 : 0).scale(isPressed ? 0.95 : 1.0)
// ⚠️ Can be expensive.blur(radius: isBlurred ? 20 : 0) // Heavy renderingCommon Animation Patterns
Button Press Animation
struct PressableButton: View { @State private var isPressed = false
var body: some View { Button("Press Me") { // Action } .scaleEffect(isPressed ? 0.9 : 1.0) .animation(.spring(response: 0.3, dampingFraction: 0.6), value: isPressed) .simultaneousGesture( DragGesture(minimumDistance: 0) .onChanged { _ in isPressed = true } .onEnded { _ in isPressed = false } ) }}Shimmer Loading Effect
struct ShimmerView: View { @State private var shimmerOffset: CGFloat = -300
var body: some View { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.3)) .frame(height: 100) .overlay( RoundedRectangle(cornerRadius: 10) .fill( LinearGradient( colors: [.clear, .white.opacity(0.5), .clear], startPoint: .leading, endPoint: .trailing ) ) .offset(x: shimmerOffset) ) .clipped() .onAppear { withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) { shimmerOffset = 300 } } }}Summary
- ✅ Implicit Animations - Use
.animation()for simple property changes - ✅ Explicit Animations - Use
withAnimationfor grouped changes - ✅ Spring Animations - Natural, physics-based movement
- ✅ Transitions - Control how views appear/disappear
- ✅ Matched Geometry - Smooth morphing between view states
- ✅ Custom Animations -
AnimatableModifierfor complex effects - ✅ Repeating Animations -
.repeatForever()for continuous motion - ✅ Performance - Keep animations smooth and purposeful
Animations make your app feel polished and professional. Use them wisely! ✨
Next: Learn SwiftUI Lists & Grids for efficiently displaying collections of data.