SwiftUI Matched Geometry Effect Crash or Bug with Lazy Grid?

Viewed 492

This is a basic hero animation setup, and seems to work 90% of the time. But randomly the animations start to not animate correctly and occasionally causes the app to crash. I made this as simple as possible to try and narrow down the issue. After hours of testing on device and simulator I cannot figure out why the animations sometimes does not animate correctly for some items in the grid, and cannot figure out the causes of the crash.

No error codes are thrown, except "Multiple inserted views in matched geometry group have isSource: true, results are undefined." But if I set the source the animation will also break, without source set the animation plays correctly.. until it doesn't.

It takes a bunch of clicking on grid items, until eventually you will see the animation not fly in correctly or even appear at all, try a different grid item and it works correctly, go back to the incorrect item and it may or may not fly in correctly, keep going and eventually it will crash.

Im wondering if I'm not understanding something about SwiftUI, or this is a bug?

The grid:

struct Prototype_RecipeGrid: View {

@Namespace var namespace
@State private var selectedRecipe: Int? = nil

var body: some View {
    
    
    ZStack {
        
        
        NavigationView {
            ScrollView {
                LazyVGrid(columns: [GridItem()], spacing: 20) {
                    ForEach(0 ..< 8) { index in
                        
                        Prototype_RecipeCard(index: index, namespace: namespace)
                            .onTapGesture { open(index) }

                    }
                }
                .padding(.horizontal)
            }
            .navigationTitle("Recipes")
        }
        .zIndex(1)
    
        
        if selectedRecipe != nil {
            Prototype_RecipeDetail(close: close, index: selectedRecipe!, namespace: namespace)
                .onTapGesture { close() }
                .zIndex(3)
        }
    }
}

private func open(_ index: Int) {
    withAnimation(.spring()) {
        selectedRecipe = index
    }
}

private func close() {
    withAnimation(.linear(duration: 0.25)) {
        selectedRecipe = nil
    }
}

}

The grid cards:

struct Prototype_RecipeCard: View {

let index: Int
let namespace: Namespace.ID

var body: some View {
    Color.clear.overlay(
    
        GeometryReader { geo in
            ZStack(alignment: .topLeading) {
                Image("047224BD-6F84-4990-9E4D-B46E425FBC5D")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .matchedGeometryEffect(id: "img\(index)", in: namespace)
                    .frame(height: 180)
                    .frame(width: geo.size.width)
                
                Prototype_RecipeTitle()
                    .matchedGeometryEffect(id: "title\(index)", in: namespace)
            }
        }
    )
    .matchedGeometryEffect(id: "detail\(index)", in: namespace)
    .clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous))
    //.transition(.move(edge: .bottom))
    .shadow(radius: 10)
    .frame(height: 180)
}

}

The hero zoomed card:

struct Prototype_RecipeDetail: View {
var close: () -> Void

let index: Int
let namespace: Namespace.ID

var body: some View {

    Color.clear.overlay(
        GeometryReader { geo in
            ScrollView(showsIndicators: false) {
                VStack {
                    ZStack(alignment: .bottomLeading) {
                        Image("047224BD-6F84-4990-9E4D-B46E425FBC5D")
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .matchedGeometryEffect(id: "img\(index)", in: namespace)
                            .frame(height: 375)
                            .frame(width: geo.size.width)

                        Prototype_RecipeTitle()
                            .matchedGeometryEffect(id: "title\(index)", in: namespace)
                    }

                    Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed risus sollicitudin, placerat massa non, facilisis tellus. Ut lorem est, sodales ut diam in, imperdiet tempor mauris. Donec sit amet elit dapibus, scelerisque tellus a, malesuada eros. Phasellus rutrum, turpis vel tempor placerat, nulla est blandit justo, sit amet faucibus libero nibh eu nisl. Curabitur quis mauris purus. Maecenas at laoreet tortor. Duis pretium auctor pulvinar. Nulla posuere risus nec sapien ornare sagittis. Nunc molestie aliquam blandit. Curabitur ut elementum felis, sit amet bibendum justo. Integer elementum et justo a bibendum. Vestibulum bibendum purus eu arcu scelerisque tempus. Donec interdum auctor neque vel tincidunt. Nulla facilisi. Quisque ut semper felis, vitae vestibulum ante. Ut et dictum tellus, ultricies condimentum orci. Aenean gravida massa vitae massa finibus suscipit. Duis sit amet ex facilisis, viverra purus non, gravida ante. Ut urna dui, consectetur ac tempus et, accumsan non est. Suspendisse sed volutpat metus. Maecenas ac neque nisl. In a efficitur purus. Curabitur ex est, vehicula vitae ipsum pulvinar, aliquet elementum purus. Mauris rhoncus maximus leo, cursus sodales elit interdum vitae. Ut et felis et purus pulvinar efficitur. Nullam pellentesque molestie nisi at rhoncus. Vestibulum sed urna non erat aliquet volutpat vel sit amet quam. Ut neque lorem, auctor a ipsum eget, volutpat consectetur urna. Phasellus eleifend maximus libero, et maximus velit lobortis non.")
                        .padding(.horizontal)
                    
                    Spacer()
                }
                .background(Color(.systemBackground))
                .clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous))
                .onTapGesture { close() }
            }
        }
    )
    .clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous))
    .matchedGeometryEffect(id: "detail\(index)", in: namespace)
    //.transition(.move(edge: .bottom))
    //.frame(height: 750)
    .edgesIgnoringSafeArea(.all)


}

}

0 Answers
Related