What is Match Geometry?
Match geometry is a powerful SwiftUI feature introduced in iOS 14 and macOS Big Sur that enables smooth, coordinated animations between views that share the same geometric properties. At its core, the matchedGeometryEffect
modifier allows you to create fluid transitions by synchronizing the position, size, and other geometric attributes of different views across your interface.
When you apply matchedGeometryEffect
to multiple views with the same identifier, SwiftUI automatically interpolates between their frames during state transitions. This creates the illusion that a single view is morphing and moving between different positions and sizes, even when you're actually showing and hiding different views.
The modifier works by establishing a geometric relationship between views, telling SwiftUI: "These views represent the same conceptual element, so animate smoothly between them when the interface changes."
Why Match Geometry Matters
Match geometry is crucial for creating polished, professional user interfaces that feel intuitive and engaging. Here's why it's so important:
Enhanced User Experience
Smooth transitions create visual continuity that guides users' eyes and maintains context, replacing jarring element appearances with intuitive flow.
Visual Hierarchy and Flow
Animated elements help users track transformations between views, making navigation feel natural and less disorienting.
Modern App Standards
Users expect fluid interfaces. Match geometry eliminates abrupt visual jumps, delivering the smooth transitions found in high-quality apps.
Reduced Cognitive Load
Smooth animations tell the story of interface changes, so users don't need to mentally reconstruct what happened.
Example 1: Card Expansion Animation
Here's a practical example showing how to create a smooth card expansion effect using match geometry:
struct CardView: View {
@State private var isExpanded = false
@Namespace private var cardNamespace
var body: some View {
VStack {
if !isExpanded {
// Collapsed card
HStack {
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue)
.frame(width: 80, height: 80)
.matchedGeometryEffect(id: "card", in: cardNamespace)
VStack(alignment: .leading) {
Text("SwiftUI Article")
.font(.headline)
.matchedGeometryEffect(id: "title", in: cardNamespace)
Text("Tap to expand")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(16)
.onTapGesture {
withAnimation(.spring()) {
isExpanded = true
}
}
} else {
// Expanded card
VStack(alignment: .leading, spacing: 20) {
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue)
.frame(height: 200)
.matchedGeometryEffect(id: "card", in: cardNamespace)
Text("SwiftUI Article")
.font(.title)
.bold()
.matchedGeometryEffect(id: "title", in: cardNamespace)
Text("This is the expanded content of our card. Notice how the blue rectangle and title smoothly animate from their collapsed positions to these expanded positions.")
.font(.body)
Button("Collapse") {
withAnimation(.spring()) {
isExpanded = false
}
}
.buttonStyle(.borderedProminent)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(16)
}
}
.padding()
}
}
In this example, both the blue rectangle and title text use matchedGeometryEffect
with unique identifiers. When the state changes, SwiftUI automatically animates these elements from their collapsed positions to their expanded positions, creating a seamless transformation.
Example 2: Tab Selection Animation
Here's another example showing how to create a smooth tab selection indicator:
struct AnimatedTabView: View {
@State private var selectedTab = 0
@Namespace private var tabNamespace
let tabs = ["Home", "Search", "Profile", "Settings"]
var body: some View {
VStack {
// Tab bar
HStack {
ForEach(tabs.indices, id: \.self) { index in
VStack {
Text(tabs[index])
.font(.system(size: 16, weight: selectedTab == index ? .semibold : .regular))
.foregroundColor(selectedTab == index ? .blue : .secondary)
if selectedTab == index {
RoundedRectangle(cornerRadius: 2)
.fill(Color.blue)
.frame(height: 3)
.matchedGeometryEffect(id: "activeTab", in: tabNamespace)
} else {
RoundedRectangle(cornerRadius: 2)
.fill(Color.clear)
.frame(height: 3)
}
}
.onTapGesture {
withAnimation(.easeInOut(duration: 0.3)) {
selectedTab = index
}
}
if index < tabs.count - 1 {
Spacer()
}
}
}
.padding(.horizontal)
// Content area
Rectangle()
.fill(Color(.systemGray6))
.overlay(
Text("Content for \(tabs[selectedTab]) tab")
.font(.title2)
.foregroundColor(.secondary)
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
This example creates a smooth sliding animation for a tab indicator. The matchedGeometryEffect
ensures that when you tap different tabs, the blue indicator smoothly slides from one position to another instead of simply appearing and disappearing.
Summary
When working with match geometry, keep these important considerations in mind:
-
Use @Namespace: Always create a namespace using
@Namespace
to group related geometry effects - Unique identifiers: Each matched geometry effect needs a unique identifier within its namespace
-
Wrap in animations: Use
withAnimation
to control the timing and style of your geometry transitions - Performance: Match geometry is optimized by SwiftUI, but avoid overusing it on complex layouts
Top comments (1)
Each matched geometry effect needs a unique identifier within its namespace