iosswiftuiswiftui-animationmatchedgeometryeffectswiftui-transition

Prevent matchedGeometryEffect from animating during transition


I am trying to present a modal that contains an element using matchedGeometryEffect. I’ve run into a problem — if I animate the transition, the matchedGeometryEffect animates as well. I’d like it not to animate the first time, but only in response to the user’s action.

Minimal example:

struct Modal: View {
    @Namespace private var modalNamespace
    var body: some View {
        HStack {
            Text("Option 1")
                .matchedGeometryEffect(id: "option1", in: modalNamespace)
            Text("Option 2")
                .matchedGeometryEffect(id: "option2", in: modalNamespace)
        }
        .frame(maxWidth: .infinity)
        .background(
            Color.blue
                .matchedGeometryEffect(id: "option2", in: modalNamespace, isSource: false)
        )
    }
}

struct ContentView: View {
    @State private var showModal: Bool = false
    var body: some View {
        ZStack {
            Button {
                withAnimation {
                    showModal = true
                }
            } label: {
                Text("Show modal")
            }
            if showModal {
                Color.gray
                    .onTapGesture {
                        showModal = false
                }
                VStack {
                    Spacer()
                    Modal()
                    Spacer()
                }
                .transition(.move(edge: .bottom))
            }
        }
    }
}

In the real app, the modal contains a segmented control that uses matchedGeometryEffect. I want to keep the animation in response to the user’s action, but I really want to remove this unwanted animation during view's transition.

Can anyone help me slide this modal in without triggering that animation? :)

enter image description here


Solution

  • One way to detach the animation for .matchedGeometryEffect from the transition is to use a state variable (a flag) to control the visibility of the background:

    By itself, this does not fix the problem, because the blue background does not animate with the rest of the view when first shown. This can be resolved by applying .geometryGroup() to the body of the Modal view:

    struct Modal: View {
        @Namespace private var modalNamespace
        @State private var isShowing = false // 👈 added
    
        var body: some View {
            HStack {
                Text("Option 1")
                    .matchedGeometryEffect(id: "option1", in: modalNamespace)
                Text("Option 2")
                    .matchedGeometryEffect(id: "option2", in: modalNamespace)
            }
            .frame(maxWidth: .infinity)
            .background { // 👈 changed
                if isShowing {
                    Color.blue
                        .matchedGeometryEffect(id: "option2", in: modalNamespace, isSource: false)
                }
            }
            .geometryGroup() // 👈 added
            .onAppear { isShowing = true } // 👈 added
            .onDisappear { isShowing = false } // 👈 added
        }
    }
    

    Animation