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.0tox: 1.0for 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.5parameter 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+