Exploring the Latest SwiftUI Animation Updates: Challenges and Triumphs

In 2023, SwiftUI has been updated to let you create even more captivating app experiences. Let's explore some of the successes and challenges from the dev side!

Exploring the Latest SwiftUI Animation Updates: Challenges and Triumphs
Photo by Sarath P Raj / Unsplash

Hey there, fellow devs and lovers of all things SwiftUI! 🚀 If you've been keeping up with Apple's quest for stunning user interfaces, you're in for a treat. In the year 2023, SwiftUI has been polished and updated to let you create even more captivating app experiences. So, get comfy, and let's delve into the world of SwiftUI animation!

Bringing UIs to Life

Remember when animations used to feel a bit stiff? Well, those days are history. At WWDC 2023, Apple dropped a bunch of animation goodies into SwiftUI's toolbox. The goal was simple: empower developers to build fluid, dynamic interfaces that just feel right.


The Power of Animation Phases

Think of animation phases like building a story step by step. These clever phases let you break complex animations into manageable chunks, making your interfaces truly shine.

// Example: Elevating views with a pinch of magic
Image(systemName: "heart.fill")
    .resizable()
    .foregroundStyle(.red)
    .frame(width: 48, height: 48)
    .phaseAnimator(Phase.allCases, trigger: animate) { content, phase in
        content
            .offset(y: phase == .move ? -20 : 0)
            .scaleEffect(phase == .scale ? 1.3 : 1)
    }
  1. Image(systemName: "heart.fill"): This creates an image view using the system symbol "heart.fill," which represents a filled heart icon.
  2. .resizable(): Makes the image resizable so that you can set its frame size.
  3. .foregroundStyle(.red): Sets the foreground color of the image to red.
  4. .frame(width: 48, height: 48): Sets the frame size of the image to a square with a width and height of 48 points.
  5. .phaseAnimator(Phase.allCases, trigger: animate) { content, phase in ... }: This part sets up phase-based animation for the image. It takes an array of Phase cases (Phase.allCases) and a trigger to start the animation.
  • content is a closure that takes a phase argument.
  • Inside the closure, content is applied with an .offset transformation based on the phase. If phase is .move, it offsets the image upwards (-20 points); if phase is .scale, it scales the image up (1.3 times).

The animation will transition between these phases based on the animate trigger. When the animate variable is set to true, the heart icon will move up and scale up as defined by the phases, creating a dynamic animation effect.

The best part? You can mix and match these phases like building blocks, creating animations that previously felt like solving a Rubik's Cube.

Getting Friendly with Keyframe Animations

But hold on, there's more! Keyframe animations are here to let you fine-tune your animations with precision. Imagine controlling the exact values of properties at different points in time—it's like being the conductor of an animation orchestra.

// Example: A heart that dances to its own rhythm
Image(systemName: "heart.fill")
    .resizable()
    .foregroundStyle(.red)
    .frame(width: 48, height: 48)
    .keyframeAnimator(initialValue: AnimationProperties(), trigger: animate) { content, value in
        content
            .scaleEffect(value.scale)
    } keyframes: { _ in
        KeyframeTrack(\.scale) {
            LinearKeyframe(0.1, duration: 0.36)
            SpringKeyframe(1.5, duration: 0.5, spring: .bouncy)
            SpringKeyframe(1, spring: .bouncy)
        }
    }

The first portion is similar to the first code snippet except

  1. .keyframeAnimator(initialValue: AnimationProperties(), trigger: animate) { content, value in ... }: This part sets up keyframe animation for the image. It takes an initialValue and a trigger to start the animation.
  2. keyframes: { _ in ... }: Defines the keyframes for the animation.
  • KeyframeTrack(\.scale) { ... }: Specifies a keyframe track for the .scale property.
  • LinearKeyframe(0.1, duration: 0.36): Sets a linear keyframe at 0.1 with a duration of 0.36 seconds.
  • SpringKeyframe(1.5, duration: 0.5, spring: .bouncy): Sets a spring keyframe with a scale factor of 1.5, a duration of 0.5 seconds, and a "bouncy" spring effect.
  • SpringKeyframe(1, spring: .bouncy): Sets another spring keyframe with a scale factor of 1 and a "bouncy" spring effect.

Facing the Realities: Challenges and Workarounds

Now, let's talk about the nitty-gritty. As excited as we are about these updates, there's a fair share of challenges. 🧩 Crafting animations can sometimes feel like putting together a puzzle with a few missing pieces. Don't forget, perfection takes time. Debugging can be a bit of a treasure hunt, but the satisfaction of nailing an animation is absolutely worth it.

Challenge #1: Syncing Phases

Coordinating animation phases can be like herding cats. But remember, every masterpiece requires some fine-tuning, and your animations are no exception.

Challenge #2: Timing is Everything

Keyframe animations offer control, but finding that perfect timing can be a balancing act. Experimenting with durations will be your new pastime.

Challenge #3: The Invisible Bug Hunt

As magical as animations are, bugs are less so. Tracking down that sneaky bug causing a hiccup in your animation is like finding a needle in a haystack. But with every bug squashed, you're one step closer to animation success.

Sharing what I've learned

In my journey with SwiftUI animations, I've encountered moments of creative triumph and the satisfaction of overcoming complex challenges. One noteworthy experience involved crafting a captivating flying hearts animation, similar to what you might have seen on Instagram.

Here's the code for the flying hearts animation:

import SwiftUI

struct ContentView: View {
    @State private var isHeartFilled = false
    @State private var hearts: [Heart] = []

    struct Heart: Identifiable {
        let id = UUID()
        var x: CGFloat
        var y: CGFloat
        var offsetY: CGFloat
    }

    enum Phase {
        case move
        case scale
    }

    @State private var currentPhase: Phase = .move

    var body: some View {
        ZStack {
            ForEach(hearts) { heart in
                Image(systemName: "heart.fill")
                    .foregroundColor(.red)
                    .font(.system(size: 20))
                    .offset(x: heart.x, y: heart.y + heart.offsetY)
                    .scaleEffect(currentPhase == .scale ? 1.3 : 1)
            }
            Image(systemName: isHeartFilled ? "heart.fill" : "heart")
                .foregroundColor(isHeartFilled ? .red : .gray)
                .font(.system(size: 30))
                .onTapGesture {
                    isHeartFilled.toggle()
                    if isHeartFilled {
                        startFloatingHearts()
                    }
                }
                .offset(y: currentPhase == .move ? -20 : 0)
        }
    }

    func startFloatingHearts() {
        let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
            if !isHeartFilled {
                timer.invalidate()
            }
            withAnimation {
                addFloatingHeart()
            }
        }
    }

    func addFloatingHeart() {
        let newHeart = Heart(
            x: CGFloat.random(in: 0..<300),
            y: CGFloat.random(in: 0..<300),
            offsetY: CGFloat.random(in: -20..<20)
        )
        hearts.append(newHeart)

        // Remove hearts after a delay (adjust duration as needed)
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            withAnimation {
                hearts.removeAll { $0.id == newHeart.id }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Here's a breakdown of the code:

  1. @State private var isHeartFilled = false: This @State property tracks whether the main heart is filled or not.
  2. @State private var hearts: [Heart] = []: This @State property maintains an array of Heart objects, which represent the floating hearts.
  3. struct Heart: Identifiable { ... }: This is a nested struct that defines the properties of each floating heart, including its id, x and y positions, and offsetY for adjusting its vertical position.
  4. Inside the ZStack:
  • The ForEach loop is used to iterate through the hearts array and display each floating heart as an Image view with the "heart.fill" symbol. The offset modifier is used to position each heart based on its x, y, and offsetY properties.
  • TheImage view displays the main heart icon, which toggles between filled and unfilled states based on the isHeartFilled state. It has an onTapGesture that toggles the isHeartFilled state and starts generating floating hearts if it becomes filled. 

5. startFloatingHearts(): This is called to start generating floating hearts using   a timer. It repeatedly calls the addFloatingHeart() function every 0.5 seconds   as long as the main heart is filled.

6. addFloatingHeart(): This creates a new Heart object with random x, y, and   offsetY properties and appends it to the hearts array. It also schedules the   removal of the heart after a 2-second delay to give the appearance of hearts   fading out.

I must admit, this journey wasn't without its fair share of challenges. Crafting the perfect flying hearts animation turned out to be a puzzle with a few missing pieces. Despite my efforts, I couldn't achieve the cool, seamless, diagonal animation as seen on Instagram. I know I'm close, and I'm still working on fine-tuning the values to achieve it  - fingers crossed. Stay tuned for upcoming posts for updates.

Embracing the SwiftUI Magic


So, whether you're a seasoned SwiftUI wizard or just dipping your toes into the magic, these animation updates are your gateway to crafting interfaces that leave users in awe. 🎩✨ Dive into animation phases for dynamic fluidity and explore keyframe animations to conduct your own animation symphony.

There you have it, folks! The future of SwiftUI animation is vibrant, dynamic, and incredibly promising. Embrace the journey, overcome the challenges, and let your creativity shine through animations that bring your interfaces to life. Happy coding! 🎉📱