In SwiftUI, MatchedGeometryEffect is a powerful modifier that allows you to create smooth, seamless animations when transitioning views between different states. It is particularly useful for animating changes in view hierarchy, position, size, and shape.
To use MatchedGeometryEffect, you need a Namespace. A Namespace is created using the @Namespace property wrapper, and it ensures that the identifier is unique within a specific scope.
@Namespace var animation
Diving In
To start, we're going to animate a text and an icon from one view to another:
struct ContentView: View {
@Namespace var animation
@State var showDetails = false
var body: some View {
ZStack {
if !showDetails {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
.matchedGeometryEffect(id: "globe", in: animation)
Text("Hello, world!")
.font(.title)
.frame(width: 150, height: 50)
.matchedGeometryEffect(id: "text", in: animation)
}
.padding()
} else {
VStack {
HStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
.matchedGeometryEffect(id: "globe", in: animation)
Text("Hello, world!")
.font(.title)
.frame(width: 150, height: 50)
.matchedGeometryEffect(id: "text", in: animation)
}
Text("Long description to display when showing the details of Hello World! ")
}
}
}
.onTapGesture {
withAnimation {
showDetails.toggle()
}
}
}
}
To match one element to another you need to use the same ID on both the first view and the second. As you can see we use fixed size here for the Text Element. We could use autolayout for this but it results in some janky animation with truncated text, which we want to avoid. And using two different fonts requires using Animatable
which we'll save for another time.
We can also add a background to our view and animate it:
struct ContentView: View {
@Namespace var animation
@State var showDetails = false
var body: some View {
ZStack {
if !showDetails {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
.matchedGeometryEffect(id: "globe", in: animation)
Text("Hello, world!")
.font(.title)
.frame(width: 150, height: 50)
.matchedGeometryEffect(id: "text", in: animation)
}
.padding()
.background {
Rectangle()
.fill(.pink)
.matchedGeometryEffect(id: "background", in: animation)
}
} else {
VStack {
HStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
.matchedGeometryEffect(id: "globe", in: animation)
Text("Hello, world!")
.font(.title)
.frame(width: 150, height: 50)
.matchedGeometryEffect(id: "text", in: animation)
}
.padding()
.background {
Rectangle()
.fill(.pink)
.matchedGeometryEffect(id: "background", in: animation)
}
Text("Long description to display when showing the details of Hello World! ")
}
}
}
.onTapGesture {
withAnimation {
showDetails.toggle()
}
}
}
}
If we want to animate the container of the view, then it's important to note that ZStack, VStack and HStack don't work well with matchedGeometryEffect, so we would need to first apply a modifier to the background of the view. As you can see it smoothly transitions between the small and large states.
In Review
MatchedGeometryEffect is a versatile and powerful tool for creating visually appealing animations. It can be particularly effective for complex view transitions and interactive UI elements, providing a smooth and intuitive user experience.
If you have questions on this modifier or if you need support building your product, contact Studio today.