Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly use matchedGeometry?

Tags:

swiftui

When I use matched geometry view modifier I always get a warning

Multiple inserted views in matched geometry group Pair<String, ID>(first: "id1", second: SwiftUI.Namespace.ID(id: 8)) have isSource: true, results are undefined.

While the animation stills works, I'd like to understand why I get this warning and how can I fix the problem.

This is the animation I built, any ideas how to get rid of the warning?

enter image description here

Using the following code

struct ContentView: View {
    @State var details = false
    @Namespace var animation
    
    var body: some View {
        ZStack {
            HStack {
                Rectangle()
                    .frame(width: 100, height: 100)
                    .matchedGeometryEffect(id: "id1", in: animation)
                    .onTapGesture {
                        withAnimation {
                            details.toggle()
                        }
                    }
                
                
                Spacer()
            }
            .zIndex(1)
            
            if details == true {
                AnotherView(details: $details, animation: animation)
                    .zIndex(2)
            }
        }
    }
}


struct AnotherView: View {
    @Binding var details: Bool
    var animation: Namespace.ID
    
    var body: some View {
        ZStack {
            Color.red
            
            Rectangle()
                .frame(width: 300, height: 300)
                .matchedGeometryEffect(id: "id1", in: animation)
                .onTapGesture {
                    withAnimation {
                        details.toggle()
                    }
                }
        }
    }
}
like image 207
Markon Avatar asked Oct 28 '20 21:10

Markon


Video Answer


2 Answers

The problem is that you have both Views on screen at the same time (even though the second one is covering the first, the first one is still there). With .matchedGeometryEffect one view is logically replacing another, so you need to remove the first View when drawing the second View. You can fix this by only drawing the first Rectangle when !details.

Also, I moved the .matchedGeometryEffect to be the first modifier of the Rectangles for a cleaner effect.

struct ContentView: View {
    @State var details = false
    @Namespace var animation
    
    var body: some View {
        ZStack {
            HStack {
                if !details {
                    Rectangle()
                        .matchedGeometryEffect(id: "id1", in: animation)
                        .frame(width: 100, height: 100)
                        .onTapGesture {
                            withAnimation {
                                details.toggle()
                            }
                        }
                }
                
                
                Spacer()
            }
            .zIndex(1)
            
            if details {
                AnotherView(details: $details, animation: animation)
                    .zIndex(2)
            }
        }
    }
}


struct AnotherView: View {
    @Binding var details: Bool
    var animation: Namespace.ID
    
    var body: some View {
        ZStack {
            Color.red
            
            Rectangle()
                .matchedGeometryEffect(id: "id1", in: animation)
                .frame(width: 300, height: 300)
                .onTapGesture {
                    withAnimation {
                        details.toggle()
                    }
                }
        }
    }
}

The .matchedGeometryEffect Documentation states (bolding added):

If inserting a view in the same transaction that another view with the same key is removed, the system will interpolate their frame rectangles in window space to make it appear that there is a single view moving from its old position to its new position. The usual transition mechanisms define how each of the two views is rendered during the transition (e.g. fade in/out, scale, etc), the matchedGeometryEffect() modifier only arranges for the geometry of the views to be linked, not their rendering.

If the number of currently-inserted views in the group with isSource = true is not exactly one results are undefined, due to it not being clear which is the source view.


demo in simulator

like image 60
vacawama Avatar answered Sep 22 '22 10:09

vacawama


The following variant works in Preview as well (proposed by @vacawama does not work for me in Preview, just in case).

Tested with Xcode 12.0 / iOS 14

demo

struct ContentView: View {
    @State var details = false
    @Namespace var animation
    
    var body: some View {
        ZStack {
            HStack {
                if !details {
                    Rectangle()
                        .matchedGeometryEffect(id: "id1", in: animation)
                        .frame(width: 100, height: 100)
                        .onTapGesture {
                            details.toggle()
                        }
                }
                Spacer()
            }.animation(.default, value: details)
            
            if details {
                AnotherView(details: $details, animation: animation)
            }
        }.animation(.default, value: details)
    }
}


struct AnotherView: View {
    @Binding var details: Bool
    var animation: Namespace.ID
    
    var body: some View {
        ZStack {
            Color.red
            
            Rectangle()
                .matchedGeometryEffect(id: "id1", in: animation)
                .frame(width: 300, height: 300)
                .onTapGesture {
                    details.toggle()
                }
        }
    }
}
like image 20
Asperi Avatar answered Sep 23 '22 10:09

Asperi