SwiftUI automatically propagates geometry changes through the view hierarchy. This behavior usually produces smooth animations with minimal configuration, but it can sometimes cause the geometry of individual child views to animate independently when you want a collection of views to move or resize as a single unit.
The geometryGroup() modifier provides a way to control this behavior by isolating a view's geometry from its parent.
geometryGroup()?geometryGroup() is a SwiftUI view modifier that isolates the position and size of a view from changes coming from its parent:
func geometryGroup() -> some ViewApple describes it as a way to isolate the geometry of a view from its parent view. When applied, SwiftUI treats the modified view as a geometry boundary. Position and size changes are resolved at that boundary before being propagated to the view's descendants.
In practice, this is particularly useful when a view contains multiple child views that should animate together.
Consider a view hierarchy like this:
Parent
└── ItemView
├── Icon
├── Title
└── SubtitleWithout a geometry group, SwiftUI can propagate position and size changes from Parent down through ItemView until they reach the views that actually draw content.
This normally works well. However, when the hierarchy changes during an animation - for example, when content is inserted or removed, the individual leaf views can receive geometry changes separately.
The result can be an animation where the contents of ItemView appear to move independently rather than behaving as one visual unit.
geometryGroup() introduces a boundary:
Parent
└── ItemView
│
│ geometry boundary
│
├── Icon
├── Title
└── SubtitleSwiftUI resolves the geometry of ItemView first and then applies that geometry to its children.
A useful way to understand geometryGroup() is to look at a view whose layout changes while it is being animated.
The following example creates a card with an icon and text. When the card expands:
import SwiftUI
struct GeometryGroupDemo: View {
@State private var isExpanded = false
var body: some View {
VStack(spacing: 40) {
Text("geometryGroup() Demo")
.font(.title.bold())
// Before: No geometry boundary
VStack(alignment: .leading) {
Text("Without geometryGroup()")
.font(.caption)
.foregroundStyle(.secondary)
CardView(isExpanded: isExpanded)
}
// After: Geometry is isolated at the card level
VStack(alignment: .leading) {
Text("With geometryGroup()")
.font(.caption)
.foregroundStyle(.secondary)
CardView(isExpanded: isExpanded)
.geometryGroup()
}
Button(isExpanded ? "Collapse Cards" : "Expand Cards") {
withAnimation(
.spring(response: 0.6, dampingFraction: 0.7)
) {
isExpanded.toggle()
}
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
.padding()
}
}
struct CardView: View {
let isExpanded: Bool
var body: some View {
HStack(spacing: 16) {
RoundedRectangle(cornerRadius: 12)
.fill(isExpanded ? .purple : .blue)
.frame(
width: isExpanded ? 80 : 50,
height: isExpanded ? 80 : 50
)
VStack(alignment: .leading, spacing: 4) {
Text("Dynamic Layout Card")
.font(.headline)
if isExpanded {
Text("""
This extra text appears only when expanded, \
changing the container's geometry dynamically.
""")
.font(.caption)
.foregroundStyle(.secondary)
.transition(.opacity)
}
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(.quaternary)
)
}
}Output:
The entire state change is wrapped in a spring animation:
withAnimation(
.spring(response: 0.6, dampingFraction: 0.7)
) {
isExpanded.toggle()
}When isExpanded changes from false to true, several things change at the same time.
The icon changes from:
.frame(width: 50, height: 50)to:
.frame(width: 80, height: 80)Its color also changes:
.fill(isExpanded ? .purple : .blue)At the same time, SwiftUI inserts another Text view:
if isExpanded {
Text("This extra text appears only when expanded...")
}This additional content changes the size of the card's layout.
So there are multiple geometry changes happening during the same animation.
geometryGroup()In the first card, there is no geometry boundary:
CardView(isExpanded: isExpanded)The card is simply part of its parent's view hierarchy.
SwiftUI normally propagates position and size changes through the hierarchy. In a simple layout, this behavior is usually exactly what you want.
However, as the hierarchy becomes more dynamic, geometry changes can be coalesced and resolved at the leaf views in ways that don't produce the visual result you expect.
geometryGroup()The second card introduces a geometry boundary:
CardView(isExpanded: isExpanded)
.geometryGroup()Now SwiftUI isolates the geometry of CardView from its parent.
Conceptually, the hierarchy looks like this:
Parent VStack
│
▼
┌─────────────────────────┐
│ CardView │
│ geometryGroup() │
│ │
│ ┌─────┐ Dynamic │
│ │ │ Layout Card │
│ │ ■ │ │
│ └─────┘ Extra text │
│ │
└─────────────────────────┘
│
▼
Child viewsThe geometry of CardView is resolved at the geometry-group boundary before being passed down to its children.
This is the important difference.
geometryGroup() does not make the animation itself smoother, and it does not replace withAnimation. Instead, it changes how SwiftUI resolves and propagates the position and size information involved in that animation.
This small example contains several characteristics that make geometryGroup() easier to understand:
The parent changes size. The card becomes taller when the additional text appears.
A child changes size. The rounded rectangle grows from 50 × 50 to 80 × 80.
A child changes appearance. The rectangle changes from blue to purple.
A child is conditionally inserted. The description appears only when the card is expanded.
Everything happens inside one animation. The changes are coordinated by the spring animation.
This is the type of situation where establishing a geometry boundary can be useful.
Think of the modifier as putting a boundary around the card:
Without geometryGroup()
Parent
│
├── CardView
│ ├── Rectangle
│ ├── Text
│ └── Text
│
└── Geometry changes can propagate
through the hierarchy
With geometryGroup()
Parent
│
▼
┌──────────────────┐
│ CardView │
│ geometryGroup() │
└──────────────────┘
│
├── Rectangle
├── Text
└── TextThe geometry group acts as a barrier. The parent resolves the position and size of the card before those values are passed to its descendants.
That is why geometryGroup() is useful when several views need to remain visually coordinated during geometry changes.
withAnimationcontrols the animation.geometryGroup()controls where SwiftUI resolves the geometry involved in that animation.
geometryGroup()You don't need to add geometryGroup() to every animated view. SwiftUI's default behavior is appropriate for most layouts.
Consider it when you have a view that:
The modifier is particularly valuable for reusable components such as cards, rows, dashboard widgets, and other composite views where the internal contents can change independently.
The key is to place the geometry boundary around the visual unit that should behave as one geometric object.
geometryGroup() is a small modifier that can solve a subtle SwiftUI animation problem.
SwiftUI normally propagates geometry changes through the view hierarchy and allows leaf views to resolve their resulting frame changes. When that behavior produces unwanted animation results, geometryGroup() introduces a boundary that causes the modified view's geometry to be resolved before its children receive it.
Use it when a collection of child views should remain visually coordinated while their parent changes size or position, particularly when those children can be inserted or removed dynamically.
In short:
Use
geometryGroup()when you want a view and its contents to animate as a single geometric unit.
Thank you for reading. If you have any questions feel free to follow me on X and send me a DM. If this article helped you, Buy me a coffee.