Skip to Content
Course content

226: Practice Exercise: Building a Custom Animation with SwiftUI

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a pattern with a lot of developers moving into SwiftUI animations: they build a beautiful layout, set up their state variables, and then get frustrated when the UI just "snaps" from one state to another. They've written the logic, but the magic is missing.

struct ExpandableCard: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 20)
                .fill(.blue)
                .frame(width: isExpanded ? 300 : 150, height: isExpanded ? 200 : 150)
                .overlay(
                    Text("Tap Me")
                        .foregroundColor(.white)
                        .opacity(isExpanded ? 0 : 1)
                )
                .onTapGesture {
                    isExpanded.toggle()
                }
        }
    }
}

The Snap Effect: Why Your View Just Jumps

If you run the code above, you'll see exactly what I'm talking about. When you tap the rectangle, it instantly jumps to the larger size. There's no glide, no easing, just a jarring teleportation of pixels. The mistake here isn't in the logic—the isExpanded state is toggling perfectly—it's that we've told SwiftUI what the end state should be, but we haven't told it how to get there.

SwiftUI doesn't animate everything by default because that would be a performance nightmare. You have to be explicit about which state changes should trigger a transition.

Triggering the Glide with Explicit Animations

The most direct way to fix this is by wrapping the state change in a withAnimation block. I usually prefer this approach because it makes it very clear exactly which user action is triggering the movement.

.onTapGesture {
    withAnimation(.spring(response: 0.5, dampingFraction: 0.6)) {
        isExpanded.toggle()
    }
}

By doing this, we aren't just saying "change the boolean"; we're saying "change this boolean, and any view that depends on it should interpolate their changes over time." I used a .spring() here instead of a linear animation because, in my experience, linear animations feel robotic and cheap. Springs give the UI a physical, tactile quality that makes an app feel polished.

Fine-Tuning with Implicit Animation Modifiers

Now, what if you want the size to animate with a spring, but you want the text opacity to fade out slowly and linearly? You can't do that with a single withAnimation block because that applies to everything tied to that state change.

This is where implicit animations come in. By attaching the .animation() modifier directly to a specific view, you tell SwiftUI: "Whenever any state changes that affects this specific view, use this specific animation."

RoundedRectangle(cornerRadius: 20)
    .fill(.blue)
    .frame(width: isExpanded ? 300 : 150, height: isExpanded ? 200 : 150)
    .animation(.spring(), value: isExpanded) // Only this view springs

    .overlay(
        Text("Tap Me")
            .foregroundColor(.white)
            .opacity(isExpanded ? 0 : 1)
            .animation(.linear(duration: 0.2), value: isExpanded) // This view fades linearly
    )

Note that I'm using the version of the modifier that takes a value. Please, avoid the deprecated .animation(.default) without a value. If you don't specify which value triggers the animation, SwiftUI will try to animate everything that changes, which often leads to weird, ghost-like bugs where views slide around when they shouldn't.




📋 Practical Task

Exercise: Building a Morphing Action Button

Your goal is to create a custom "Action Button" that morphs from a circular icon button into a wide "Confirm" button when tapped. This will test your ability to combine different animation types for a polished feel.

Requirements:

  • Create a @State variable called isConfirmed.
  • The view should start as a circle (width and height roughly 60pt) with a "Checkmark" icon inside.
  • When tapped, the button should expand its width to 200pt and change its shape to a rounded rectangle.
  • The text should change from an empty string (or be hidden) to "Confirm Purchase".
  • Crucial: Use a .spring() animation for the frame/shape change to give it a "pop" effect, but use a .easeIn animation for the text opacity so it fades in smoothly without bouncing.
  • Ensure you use the value: parameter in your animation modifiers to avoid global animation leaks.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.