Matched Geometry Effect implementation in iOS SwiftUI app

NOVASOLUTIONS.TECHNOLOGY is engaged in the development, support and maintenance of iOS, Android, PWA mobile applications. We have extensive experience and expertise in publishing mobile applications in popular markets like Google Play, App Store, Amazon, AppGallery and others.
Development and support of all types of mobile applications:
Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Showing 1 of 1 servicesAll 1735 services
Matched Geometry Effect implementation in iOS SwiftUI app
Medium
from 1 business day to 3 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_mobile-applications_feedme_467_0.webp
    Development of a mobile application for FEEDME
    756
  • image_mobile-applications_xoomer_471_0.webp
    Development of a mobile application for XOOMER
    624
  • image_mobile-applications_rhl_428_0.webp
    Development of a mobile application for RHL
    1052
  • image_mobile-applications_zippy_411_0.webp
    Development of a mobile application for ZIPPY
    947
  • image_mobile-applications_affhome_429_0.webp
    Development of a mobile application for Affhome
    862
  • image_mobile-applications_flavors_409_0.webp
    Development of a mobile application for the FLAVORS company
    445

Implementing Matched Geometry Effect in iOS Applications (SwiftUI)

matchedGeometryEffect is a SwiftUI mechanism for animating geometric transitions between two Views with the same identifier. When toggling state (show/hide, expand/collapse), SwiftUI interpolates position, size, and anchor point between paired elements—the result looks like smooth "flowing" of one into the other.

A powerful tool. And a regular source of unexpected artifacts if you don't understand how it works internally.

Basic Principle and Typical Cases

matchedGeometryEffect requires two things: a Namespace (shared identifier space) and matching id on the View pair. isSource: true marks the View as the source for geometry calculation.

struct ExpandableCard: View {
    @State private var isExpanded = false
    @Namespace private var cardNamespace

    var body: some View {
        if isExpanded {
            // Full-screen view
            VStack {
                Image("product")
                    .resizable()
                    .matchedGeometryEffect(id: "product-image", in: cardNamespace)
                    .frame(maxWidth: .infinity)
                    .frame(height: 300)
                Text("Detailed description...")
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .onTapGesture { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
                isExpanded = false
            }}
        } else {
            // Card in list
            HStack {
                Image("product")
                    .resizable()
                    .matchedGeometryEffect(id: "product-image", in: cardNamespace)
                    .frame(width: 80, height: 80)
                    .cornerRadius(8)
                Text("Brief title")
            }
            .onTapGesture { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
                isExpanded = true
            }}
        }
    }
}

Pitfalls and How to Avoid Them

Problem 1: Both Views render simultaneously. matchedGeometryEffect doesn't hide elements automatically—if both Views exist in the hierarchy simultaneously, you see both. The if/else pattern above is correct: only one variant exists at a time.

For lists with many elements (LazyVGrid + detail overlay), the correct structure is:

ZStack {
    LazyVGrid(columns: ...) {
        ForEach(products) { product in
            ProductCard(product: product, namespace: gridNamespace,
                        isSelected: selectedProduct?.id == product.id)
                .onTapGesture { withAnimation(.spring()) { selectedProduct = product } }
        }
    }

    if let selected = selectedProduct {
        ProductDetail(product: selected, namespace: gridNamespace)
            .onTapGesture { withAnimation(.spring()) { selectedProduct = nil } }
    }
}

In ProductCard: if isSelected == true, hide the original via .opacity(0)—the position in grid remains but the element is invisible. matchedGeometryEffect continues using its geometry as the source.

Image(product.imageName)
    .matchedGeometryEffect(id: "product-\(product.id)", in: gridNamespace,
                           isSource: !isSelected)
    .opacity(isSelected ? 0 : 1)

Problem 2: Namespace only works within one View tree. @Namespace can't be passed through NavigationLink to another screen—they're in separate hierarchies. matchedGeometryEffect works only within one body or by passing Namespace.ID as a parameter down the tree. For cross-screen transitions via NavigationStack, use iOS 18 NavigationTransition API or custom AnyTransition.

Problem 3: Layout loop. If two Views with isSource: true and the same id exist in one container simultaneously, SwiftUI enters a layout loop. Console: "Bound preference ... tried to update multiple times per frame". Always one source only.

Problem 4: Animation clipped. Views inside List or ScrollView are clipped by container bounds. When a card expands, animation clips at the list edge. Solution: place the detail view outside the List in a ZStack above it, as in the pattern above.

Animated Custom Tab Bar

Popular use case: the active tab indicator smoothly moves between tabs:

struct AnimatedTabBar: View {
    @State private var selectedTab = 0
    @Namespace private var tabNamespace

    let tabs = ["house", "magnifyingglass", "heart", "person"]

    var body: some View {
        HStack {
            ForEach(tabs.indices, id: \.self) { index in
                ZStack {
                    if selectedTab == index {
                        RoundedRectangle(cornerRadius: 12)
                            .fill(Color.blue.opacity(0.15))
                            .matchedGeometryEffect(id: "tab-indicator", in: tabNamespace)
                            .frame(width: 48, height: 36)
                    }
                    Image(systemName: tabs[index])
                        .foregroundColor(selectedTab == index ? .blue : .gray)
                }
                .frame(maxWidth: .infinity)
                .onTapGesture {
                    withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                        selectedTab = index
                    }
                }
            }
        }
        .padding(8)
        .background(Color(.systemBackground))
    }
}

The indicator is one View with matchedGeometryEffect, which "jumps" between tab positions via spring. This works because matchedGeometryEffect with one id in ForEach applies to the single element where the condition is true.

Timeline

Expandable card with matchedGeometryEffect (single card) takes 0.5–1 day. LazyGrid with detail overlay and correct visibility handling takes 1–2 days. Animated tab bar or custom navigation indicator takes a few hours. Cost is calculated individually.