Skip to content
All essays
iOSMarch 27, 202515 min

SwiftUI Animations Advanced: Create Engaging Experiences

Master advanced SwiftUI animations. Springs, transitions, and gesture-driven animations.

Ü
Ümit Uz
Mobile & Full Stack Developer

Animations bring your SwiftUI apps to life. Learn advanced animation techniques to create engaging user experiences.

Animation Basics

Implicit Animation

swift
struct BasicAnimation: View {
    @State private var scale: CGFloat = 1.0

    var body: some View {
        Circle()
            .fill(Color.blue)
            .frame(width: 100, height: 100)
            .scaleEffect(scale)
            .animation(.easeInOut(duration: 0.3), value: scale)
            .onTapGesture {
                scale = scale == 1.0 ? 1.5 : 1.0
            }
    }
}

Explicit Animation

swift
struct ExplicitAnimation: View {
    @State private var offset = CGSize.zero

    var body: some View {
        Circle()
            .fill(Color.orange)
            .frame(width: 100, height: 100)
            .offset(offset)
            .onTapGesture {
                withAnimation(.spring()) {
                    offset = offset == .zero ? CGSize(width: 100, height: 0) : .zero
                }
            }
    }
}

Spring Animations

Custom Springs

swift
struct SpringAnimations: View {
    @State private var isAnimating = false

    var body: some View {
        VStack(spacing: 30) {
            // Smooth spring
            Circle()
                .fill(Color.blue)
                .frame(width: 60, height: 60)
                .scaleEffect(isAnimating ? 1.5 : 1.0)
                .animation(.spring(response: 0.6, dampingFraction: 0.8), value: isAnimating)

            // Bouncy spring
            Circle()
                .fill(Color.green)
                .frame(width: 60, height: 60)
                .scaleEffect(isAnimating ? 1.5 : 1.0)
                .animation(.spring(response: 0.3, dampingFraction: 0.4), value: isAnimating)

            // Snappy spring
            Circle()
                .fill(Color.orange)
                .frame(width: 60, height: 60)
                .scaleEffect(isAnimating ? 1.5 : 1.0)
                .animation(.spring(response: 0.3, dampingFraction: 0.6), value: isAnimating)
        }
        .onTapGesture {
            isAnimating.toggle()
        }
    }
}

Keyframe Animations

swift
struct KeyframeAnimation: View {
    @State private var isAnimating = false

    var body: some View {
        RoundedRectangle(cornerRadius: isAnimating ? 30 : 10)
            .fill(LinearGradient(
                colors: [.blue, .purple],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            ))
            .frame(width: 150, height: 150)
            .rotationEffect(.degrees(isAnimating ? 180 : 0))
            .animation(.keyframe(
                duration: 1.5,
                values: [
                    KeyframeTrack(rotation: .keyframes(
                        values: [0, 90, 180, 270, 360],
                        times: [0, 0.25, 0.5, 0.75, 1.0]
                    ))
                ]
            ), value: isAnimating)
            .onTapGesture {
                isAnimating.toggle()
            }
    }
}

Transitions

Custom Transitions

swift
struct CustomTransitions: View {
    @State private var showDetail = false

    var body: some View {
        VStack {
            if showDetail {
                RoundedRectangle(cornerRadius: 20)
                    .fill(Color.blue)
                    .frame(width: 300, height: 200)
                    .transition(.asymmetric(
                        insertion: .scale.combined(with: .opacity),
                        removal: .opacity
                    ))
            }

            Button("Toggle") {
                withAnimation(.spring()) {
                    showDetail.toggle()
                }
            }
        }
    }
}

Slide Transition

swift
struct SlideTransition: View {
    @State private var showCard = false

    var body: some View {
        ZStack {
            Color.gray.opacity(0.2)
                .ignoresSafeArea()

            if showCard {
                RoundedRectangle(cornerRadius: 20)
                    .fill(Color.white)
                    .frame(width: 300, height: 400)
                    .shadow(radius: 10)
                    .transition(.move(edge: .bottom).combined(with: .opacity))
            }
        }
        .onTapGesture {
            withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
                showCard.toggle()
            }
        }
    }
}

Gesture-Driven Animations

Drag to Dismiss

swift
struct DragToDismiss: View {
    @State private var offset: CGFloat = 0
    @State private var isDragging = false

    var body: some View {
        GeometryReader { geometry in
            RoundedRectangle(cornerRadius: 20)
                .fill(Color.blue)
                .frame(width: 300, height: 400)
                .offset(y: offset)
                .gesture(
                    DragGesture()
                        .onChanged { value in
                            isDragging = true
                            offset = value.translation.height
                        }
                        .onEnded { value in
                            isDragging = false
                            withAnimation(.spring()) {
                                if value.translation.height > 100 {
                                    offset = geometry.size.height
                                } else {
                                    offset = 0
                                }
                            }
                        }
                )
                .animation(.spring(response: 0.4, dampingFraction: 0.8), value: isDragging)
        }
    }
}

Swipe Gestures

swift
struct SwipeGestures: View {
    @State private var offset: CGFloat = 0
    @State private var color: Color = .blue

    var body: some View {
        RoundedRectangle(cornerRadius: 15)
            .fill(color)
            .frame(width: 300, height: 80)
            .offset(x: offset)
            .gesture(
                DragGesture()
                    .onChanged { value in
                        if value.translation.width > 0 {
                            offset = value.translation.width
                            color = .green
                        } else {
                            offset = value.translation.width
                            color = .red
                        }
                    }
                    .onEnded { value in
                        withAnimation(.spring()) {
                            if abs(value.translation.width) > 100 {
                                offset = value.translation.width > 0 ? 200 : -200
                            } else {
                                offset = 0
                                color = .blue
                            }
                        }
                    }
            )
            .animation(.spring(response: 0.4, dampingFraction: 0.7), value: offset)
    }
}

Matching Geometry Effect

Hero Animation

swift
struct HeroAnimation: View {
    @Namespace private var hero
    @State private var isExpanded = false

    var body: some View {
        VStack {
            if isExpanded {
                RoundedRectangle(cornerRadius: 20)
                    .fill(LinearGradient(
                        colors: [.purple, .pink],
                        startPoint: .topLeading,
                        endPoint: .bottomTrailing
                    ))
                    .frame(width: 300, height: 300)
                    .matchedGeometryEffect(id: "card", in: hero)
            } else {
                RoundedRectangle(cornerRadius: 10)
                    .fill(LinearGradient(
                        colors: [.purple, .pink],
                        startPoint: .topLeading,
                        endPoint: .bottomTrailing
                    ))
                    .frame(width: 150, height: 150)
                    .matchedGeometryEffect(id: "card", in: hero)
            }
        }
        .onTapGesture {
            withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
                isExpanded.toggle()
            }
        }
    }
}

Sequential Animations

Animation Sequence

swift
struct SequentialAnimations: View {
    @State private var scale1: CGFloat = 1.0
    @State private var scale2: CGFloat = 1.0
    @State private var scale3: CGFloat = 1.0

    var body: some View {
        HStack(spacing: 20) {
            Circle()
                .fill(Color.blue)
                .frame(width: 60, height: 60)
                .scaleEffect(scale1)

            Circle()
                .fill(Color.green)
                .frame(width: 60, height: 60)
                .scaleEffect(scale2)

            Circle()
                .fill(Color.orange)
                .frame(width: 60, height: 60)
                .scaleEffect(scale3)
        }
        .onTapGesture {
            animateSequentially()
        }
    }

    func animateSequentially() {
        scale1 = 1.0
        scale2 = 1.0
        scale3 = 1.0

        withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
            scale1 = 1.5
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
            withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
                scale2 = 1.5
            }
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
            withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
                scale3 = 1.5
            }
        }
    }
}

Timeline View

Countdown Animation

swift
struct CountdownAnimation: View {
    var body: some View {
        TimelineView(.animation(minimumInterval: 1.0)) { context in
            let seconds = context.date.timeIntervalSince1970
            let remaining = 10 - Int(seconds) % 10

            ZStack {
                Circle()
                    .stroke(Color.gray.opacity(0.2), lineWidth: 20)
                    .frame(width: 200, height: 200)

                Circle()
                    .trim(from: 0, to: CGFloat(remaining) / 10.0)
                    .stroke(
                        AngularGradient(
                            gradient: Gradient(colors: [.blue, .purple]),
                            center: .center
                        ),
                        style: StrokeStyle(lineWidth: 20, lineCap: .round)
                    )
                    .frame(width: 200, height: 200)
                    .rotationEffect(.degrees(-90))
                    .animation(.linear, value: remaining)

                Text("\(remaining)")
                    .font(.system(size: 72, weight: .bold))
            }
        }
    }
}

Best Practices

  1. 1Springs: Use spring animations for natural feel
  2. 2Performance: Animate view properties, not view hierarchy
  3. 3Consistency: Use consistent animation timing
  4. 4Feedback: Provide visual feedback for interactions
  5. 5Accessibility: Respect reduced motion preferences
  6. 6Testing: Test animations on different devices
  7. 7Optimization: Use withAnimation sparingly
  8. 8UX: Animations should enhance, not distract

Advanced animations create delightful user experiences in SwiftUI!

Related essays

Next essay
Push Notifications in Swift: Complete Implementation Guide