Use match geometry effect when navigating between views using a navigation link

805 Views Asked by At

My home view contains a CustomView that opens a detailed view via a NavigationLink when tapped. The detailed view also contains the CustomView, just in a different location.

Can I use the match geometry effect to transition/animate the location of the CustomView when the navigation link is clicked?

struct HomeView: View {
    @Namespace var namespace
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Top")
                NavigationLink {
                    DetailView(namespace: namespace)
                } label: {
                    CustomView()
                        .matchedGeometryEffect(id: "testId", in: namespace)
                }
                Text("Bottom")
            }
        }
    }
}
struct DetailView: View {
    var namespace: Namespace.ID
    
    var body: some View {
        VStack {
            CustomView()
                .matchedGeometryEffect(id: "testId", in: namespace)
            Text("Details")
            Spacer()
        }
    }
}
2

There are 2 best solutions below

2
On

I'm going to say, "no, you can't use .matchedGeometryEffect to animate the position of a view across a NavigationStack transition."

Some details: first of all, the .matchedGeometryEffect will not work without an animation block. So you could rewrite your code to something like this:

struct HomeView: View {
    @Namespace var namespace
    @State private var showDetail = false
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Top")
                Button {
                    withAnimation() {
                        showDetail.toggle()
                    }
                } label: {
                    CustomView
                        .matchedGeometryEffect(id: "testId", in: namespace)
                }
                Text("Bottom")
            }
            .navigationDestination(isPresented: $showDetail) {
                DetailView(namespace: namespace)
            }
        }
    }
}

private struct DetailView: View {
    var namespace: Namespace.ID
    
    var body: some View {
        VStack {
            CustomView()
                .matchedGeometryEffect(id: "testId", in: namespace)
            Text("Details")
            Spacer()
        }
    }
}

This gives you an opportunity to use withAnimation in a way that matchedGeometryEffect requires. However, you still don't get the animation you want, leading me to the conclusion above.

I would speculate that Navigation transitions are different from normal SwiftUI transitions. A clue supporting this is that any .transition you could apply to the things in DetailView do not trigger during transition. Another clue is that using withAnimation(.easeIn(duration: 5)) does not give you a 5 second transition.

Your best bet might be to not use NavigationStack. Just use a conditional (with animated transition) to switch between the two layouts. I would also recommend you spend a few days studying the excellent resources on the Swift UI Lab site, with this being a particularly relevant page for you.

I hope that helps!

2
On

Using the animation modifier can impact the performance, may not be the best solution, but still you can give it a try

struct HomeView: View {
    @Namespace var namespace
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Top")
                NavigationLink {
                    DetailView(namespace: namespace)
                } label: {
                    CustomView()
                        .matchedGeometryEffect(id: "testId", in: namespace)
                        .animation(.easeInOut(duration: 0.5))
                }
                Text("Bottom")
            }
        }
    }
}

struct DetailView: View {
    var namespace: Namespace.ID
    
    var body: some View {
        VStack {
            CustomView()
                .matchedGeometryEffect(id: "testId", in: namespace)
                .animation(.easeInOut(duration: 0.5))
            Text("Details")
            Spacer()
        }
    }
}