Crafting App Store-Style Card Animations with SwiftUI
Recreate the App Store Today tab's signature card-to-fullscreen transition using matchedGeometryEffect, Namespace, and clean view composition.
Charith 'Alex' Gunasekara
Head of Development & Engineering
The App Store's Today tab is a masterclass in smooth, immersive interaction — especially the way a card expands into a full-screen view with one fluid motion. The good news: SwiftUI gives you everything you need to recreate it with matchedGeometryEffect, a shared Namespace, and a little view composition.
The card list
Start with a model and a reusable card.
struct CardModel: Identifiable {
let id = UUID()
let title: String
let color: Color
}
struct CardView: View {
let card: CardModel
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(card.color)
Text(card.title)
.font(.title2)
.foregroundColor(.white)
}
}
}The home view declares a @Namespace (the glue for matchedGeometryEffect) and tracks which card is selected and whether the detail is showing. On tap, it sets the selection and animates with a spring; the matching geometry IDs let the small card and the full-screen card share one continuous transition.
struct HomeListView: View {
@Namespace var animation
@State private var selectedCard: CardModel?
@State private var showDetail = false
let cards = [
CardModel(title: "SwiftUI Magic", color: .blue),
CardModel(title: "iOS Animations", color: .green),
CardModel(title: "UI Components", color: .purple)
]
var body: some View {
NavigationStack {
ZStack {
ScrollView {
VStack(spacing: 20) {
ForEach(cards) { card in
CardView(card: card)
.matchedGeometryEffect(id: card.id, in: animation)
.frame(height: 200)
.onTapGesture {
selectedCard = card
withAnimation(
.spring(response: 0.4, dampingFraction: 0.8)
) {
showDetail = true
}
}
}
}
.padding()
}
.navigationTitle("Cards")
.navigationBarTitleDisplayMode(.large)
if let card = selectedCard, showDetail {
FullscreenCardView(
card: card,
animation: animation,
show: $showDetail,
selectedCard: $selectedCard
)
}
}
}
}
}The full-screen card
The detail view reuses the same matchedGeometryEffect id, so the colored header appears to grow out of the tapped card. The close button reverses the spring and clears the selection just after the animation settles.
import SwiftUI
struct FullscreenCardView: View {
let card: CardModel
var animation: Namespace.ID
@Binding var show: Bool
@Binding var selectedCard: CardModel?
var body: some View {
ZStack {
VStack(spacing: 0) {
// Top colored section
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 0)
.fill(card.color)
.matchedGeometryEffect(id: card.id, in: animation)
.frame(height: 200)
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 0) {
Spacer()
Button(action: {
withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
show = false
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
selectedCard = nil
}
}) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 32))
.foregroundColor(.white)
.padding(16)
}
}
.padding(.trailing, 16)
.padding(.top, 50)
Text(card.title)
.font(.largeTitle)
.bold()
.foregroundColor(.white)
.padding(.leading, 16)
Text("This is subtitle")
.font(.subheadline)
.foregroundColor(.white)
.padding(.leading, 16)
}
}
// Bottom white content
VStack(alignment: .leading, spacing: 8) {
Text("Detailed description goes here.")
.font(.body)
.foregroundColor(.black)
Text("You can add more components like buttons, images, or even scrollable content.")
.font(.subheadline)
.foregroundColor(.gray)
Spacer()
}
.padding(.leading, 16)
.padding(.top, 20)
.background(Color.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.ignoresSafeArea()
}
.zIndex(1)
.navigationBarHidden(true)
}
}Takeaway
Recreating the App Store-style card animation is a great way to explore advanced SwiftUI motion: matchedGeometryEffect for the shared transition, view transitions for the reveal, and layout composition for the result. With clean state management and reusable components, you get a polished, immersive experience that feels genuinely native.