What You Will Build

A card that flips between front and back faces with a realistic 3D rotation around the Y-axis. The front face hides at 90 degrees while the back face reveals, creating a seamless card flip. This is used in memory games, flashcard apps, credit card displays, and settings panels with a "reveal more info" interaction.

Why This Pattern Matters

The .rotation3DEffect modifier gives SwiftUI real perspective-correct 3D transforms. The challenge is coordinating two faces so that one disappears exactly as the other appears at the 90-degree midpoint. This tutorial shows the delayed animation technique that makes the illusion seamless.

Key SwiftUI Concepts

  • rotation3DEffect — applies a 3D rotation around any axis with perspective.
  • Delayed animation — the front face animates immediately while the back delays by 0.35s (and vice versa) to sync at the midpoint.
  • ZStack layering — both faces exist simultaneously, but only one is visible at any time.

Step 1: Build a Reusable Card Face

struct FlipCardSide: View {
    var text: String
    var color: Color
    var isTrue: CGFloat    // rotation when flipped
    var isFalse: CGFloat   // rotation when not flipped
    var isFlipped: Bool

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 20)
                .frame(width: 200, height: 300)
                .foregroundColor(color)
                .overlay {
                    RoundedRectangle(cornerRadius: 20)
                        .stroke(lineWidth: 2)
                        .foregroundColor(.white.opacity(0.5))
                }
            Text(text).bold().font(.title)
                .foregroundStyle(.white)
        }
        .rotation3DEffect(
            .degrees(isFlipped ? isTrue : isFalse),
            axis: (x: 0.0, y: 1.0, z: 0.0)
        )
    }
}

Step 2: Assemble the Flip Card

The key is the asymmetric animation delays. When flipping forward, the front face rotates immediately while the back face waits 0.35 seconds before starting its rotation. This ensures the back face does not appear until the front has rotated past 90 degrees:

struct CardRotationDemo: View {
    @State var isFlipped = false

    var body: some View {
        VStack(spacing: 30) {
            Text("Tap to Flip")
                .font(.title2.bold())

            ZStack {
                FlipCardSide(
                    text: "BACK", color: .indigo,
                    isTrue: 0, isFalse: -90, isFlipped: isFlipped
                )
                .animation(
                    isFlipped ? .linear.delay(0.35) : .linear,
                    value: isFlipped
                )

                FlipCardSide(
                    text: "FRONT", color: .orange,
                    isTrue: 90, isFalse: 0, isFlipped: isFlipped
                )
                .animation(
                    isFlipped ? .linear : .linear.delay(0.35),
                    value: isFlipped
                )
            }
            .onTapGesture {
                withAnimation(.easeIn) { isFlipped.toggle() }
            }
        }
    }
}

How the Delay Trick Works

When isFlipped becomes true:

  • The FRONT face rotates from 0 to 90 degrees immediately (disappearing at 90).
  • The BACK face waits 0.35s, then rotates from -90 to 0 degrees (appearing from the other side).

When isFlipped becomes false, the delays swap. This ensures only one face is ever visible, creating the illusion of a single physical card.

Tips and Pitfalls

  • Why not just use opacity? Fading between faces does not look like a real flip. The 3D rotation with coordinated delays creates genuine depth.
  • Adjust the delay to match your animation duration. If you use a 0.5s animation, set the delay to half that (0.25s).
  • Axis options: Change y: 1.0 to x: 1.0 for a vertical flip, or use both axes for a diagonal tumble.
  • Add perspective: The default perspective is fine for small cards. For full-screen flips, add perspective: 0.5 parameter to reduce the fish-eye effect.
  • Content mirroring: Text on the back face is not mirrored because the back face starts at -90 and rotates to 0 (not 180). This keeps text readable.

iOS Version: iOS 15+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.