← Back to archive

Development · SwiftUI

Connecting views with matchedGeometryEffect

Use a shared namespace and ID to animate smoothly between different views.

Read the Korean original on Velog ↗English translation

Development archive cover

When one view replaces another, a basic animation may not make the two states feel connected. matchedGeometryEffect lets SwiftUI treat them as representations of the same visual element.

Matched geometry

A typical animation interpolates changes within a view. Matched geometry coordinates changes between views. Give the related views the same id within a shared @Namespace so SwiftUI can connect their geometry.

struct MatchedGeometryEffectStudy: View {
    
    @State private var isClicked: Bool = false
    @Namespace private var namespace
    
    var body: some View {
        VStack {
            if !isClicked {
                RoundedRectangle(cornerRadius: 12)
                	// Match this view for the animation
                    .matchedGeometryEffect(id: "rectangle", in: namespace)
                    .frame(width: 100, height: 100)
            }
            Spacer()
            if isClicked {
                RoundedRectangle(cornerRadius: 12)
					// Match this view for the animation
                    .matchedGeometryEffect(id: "rectangle", in: namespace)
                    .frame(width: 200, height: 200)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.red)
        .onTapGesture {
            withAnimation(.easeInOut) {
                isClicked.toggle()
            }
        }
    }
}

The same technique can animate the selection indicator in a segmented control.

struct MatchedGeometryEffectStudy2: View {
    
    let categories: [String] = ["Home", "Popular", "Saved"]
    @State private var selected: String = "Home"
    @Namespace private var namespace2
    
    var body: some View {
        HStack {
            ForEach(categories, id: \.self) { category in
                ZStack {
                    if selected == category {
                        RoundedRectangle(cornerRadius: 12)
                            .fill(Color.blue).opacity(0.3)
							// Match this view for the animation
                            .matchedGeometryEffect(id: "category", in: namespace2)
                    }
                    Text(category)
                }
                .frame(maxWidth: .infinity)
                .frame(height: 56)
                .onTapGesture {
                    withAnimation(.spring()) {
                        selected = category
                    }
                }
            }
        }
    }
}

← Back to archive