iosanimationswiftuimatchedgeometryeffect

SwiftUI: matchedGeometryEffect and clipShape Animation Bug


Say I have a container view that I use to apply consistent styling to various contained views.

struct FormattedContainer<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
            content
                .padding()
                .background(Color.gray.opacity(0.2))
    }
}

I use it to enclose a couple of views that use matchedGeometryEffect to animate from one container to another.

struct ContentView: View {
    @Namespace var animation
    @State var isFlipped: Bool = false

    var body: some View {
        VStack {
            FormattedContainer {
                HStack {
                    Rectangle()
                    if isFlipped {
                        Text("This is the active container")
                            .layoutPriority(1)
                            .matchedGeometryEffect(id: "id", in: animation)
                    }
                }
            }
            Spacer(minLength: 40)
            FormattedContainer {
                HStack{
                    if !isFlipped {
                        Text("This is the active container")
                            .layoutPriority(1)
                            .matchedGeometryEffect(id: "id", in: animation)
                    }
                    Rectangle()
                }
            }
        }
        .contentShape(Rectangle())
        .padding()
        .onTapGesture {
            withAnimation {
                isFlipped.toggle()
            }
        }
    }
}

This works well and look like this:

Expected Animation Behavior

If I add a clip shape to my container to give it rounded corners:

struct FormattedContainer<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
            content
                .padding()
                .background(Color.gray.opacity(0.2))
                .clipShape(RoundedRectangle(cornerRadius: 20)) <----- added line
    }
}

The animation disappears when moving from container to container:

Buggy Animation Behavior

Any ideas how to fix the formatted container so it has rounded corners and the animation doesn't disappear?


Solution

  • You should not clip content. You can add an in: parameter to specify what shape the background should be in:

    .background(Color.gray.opacity(0.2), in: .rect(cornerRadius: 20))
    // remove clipShape!
    

    If you do need clipShape to clip other content, you need to use a third Text on top of everything to match the geometry of one of the other Texts.

    VStack {
        FormattedContainer {
            HStack {
                Rectangle()
                if isFlipped {
                    Text("This is the active container")
                        .hidden()
                        .layoutPriority(1)
                        .matchedGeometryEffect(id: true, in: animation)
                }
            }
        }
        Spacer(minLength: 40)
        FormattedContainer {
            HStack{
                if !isFlipped {
                    Text("This is the active container")
                        .hidden()
                        .layoutPriority(1)
                        .matchedGeometryEffect(id: false, in: animation)
                }
                Rectangle()
            }
        }
    }
    .contentShape(Rectangle())
    .padding()
    .onTapGesture {
        withAnimation {
            isFlipped.toggle()
        }
    }
    .overlay {
        // this is the only Text you see!
        // it matches the geometry of either the top container's or bottom container's text depending on isFlipped
        Text("This is the active container")
            .matchedGeometryEffect(id: isFlipped, in: animation, isSource: false)
    }