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

2

12

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()
        }
    }
}
Mcbee answered 6/6, 2023 at 2:46 Comment(0)
M
1

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!

Ming answered 29/9, 2023 at 18:24 Comment(2)
what if we add animation modifier to matchedGeometryEffect?Zippy
Could you be a bit more specific, please? I don't follow.Ming
Z
0

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()
        }
    }
}
Zippy answered 3/10, 2023 at 6:26 Comment(2)
Although this does give the view an animation, it starts in a random place. Bottom right corner for the pushed view, the when popping the view it animated from the centre up to the corner and back again.Cullin
@Cullin This may be happening because the match geometry effect relies on the stable names of views to accurately animate themZippy

© 2022 - 2024 — McMap. All rights reserved.