MatchedGeometryEffect giving undesirable ZIndex results

190 Views Asked by At

In SwiftUI, I've created a 2x2 Grid of Colors in a ZStack. I want to click a color and have it expanded into a DetailView using .matchedGeometryEffect in my ZStack without any kind of transparency. This was very easy to do using the following code:

import SwiftUI


struct ContentView: View {
    let colors: [Color] = [.red, .orange, .green, .indigo]
    @Namespace var nameSpace
    @State var showDetailView = false
    @State var selectedColorIndex: Int? = nil
    
    var body: some View {
        ZStack {
            Color.blue
            Grid(horizontalSpacing: 0, verticalSpacing: 0) {
                ForEach(0..<2) { row in
                    GridRow {
                        ForEach(0..<2) { column in
                            let index = (row * 2) + column
                            colors[index]
                                .matchedGeometryEffect(id: index, in: nameSpace)
                                .onTapGesture {
                                    withAnimation {
                                        selectedColorIndex = index
                                        showDetailView = true
                                    }
                                }
                                .zIndex(selectedColorIndex == index ? 1 :  0)
                        }
                    }
                }
            }
            if showDetailView {
                if let index = selectedColorIndex {
                    colors[index]
                        .matchedGeometryEffect(id: index, in: nameSpace)
                        .onTapGesture {
                            withAnimation {
                                showDetailView = false
                            }
                        }
                        .zIndex(1)
                }
            }
        }
        .frame(width: 300, height: 300)
    }
}

However, this presents the following error.

Multiple inserted views in matched geometry group Pair<Int, ID>(first: 1, second: SwiftUI.Namespace.ID(id: 86)) have isSource: true, results are undefined.

I understand the reasoning behind this error. I still have the smaller view present while the larger detail View is presented. Conditionally removing the specific smaller view with logic (if statement) and thus getting rid of the error message is very easy. However it breaks my ZIndex and nothing I've tried has led me to a solution to maintain my property ZIndex layout.

What I want to achieve (error free): Good example

Here is one of many things I've tried. It gets rid of the error, but breaks the effect of having the Color expand over the others.

ZStack {
            Color.blue
            Grid(horizontalSpacing: 0, verticalSpacing: 0) {
                ForEach(0..<2) { row in
                    GridRow {
                        ForEach(0..<2) { column in
                            let index = (row * 2) + column
                            if index != selectedColorIndex {
                                colors[index]
                                    .matchedGeometryEffect(id: index, in: nameSpace)
                                    .onTapGesture {
                                        withAnimation {
                                            selectedColorIndex = index
                                            showDetailView = true
                                        }
                                    }
                                    .zIndex(selectedColorIndex == index ? 1 :  0)
                            } else { Color.clear}
                        }
                    }
                }
            }
            if showDetailView {
                if let index = selectedColorIndex {
                    colors[index]
                        .matchedGeometryEffect(id: index, in: nameSpace)
                        .onTapGesture {
                            withAnimation {
                                showDetailView = false
                                selectedColorIndex = nil
                            }
                        }
                        .zIndex(1)
                }
            }
        }
        .frame(width: 300, height: 300)

Example with no more error, but not what I want visually: Less good example

1

There are 1 best solutions below

1
On BEST ANSWER

I tried a number of things as well, and I found this to work.

Change your first .matchedGeometryEffect from:

.matchedGeometryEffect(id: index, in: nameSpace)

to:

.matchedGeometryEffect(id: index, in: nameSpace, isSource: !showDetailView)

This ensures that only one is in effect with isSource == true at all times.

Then add:

.transition(.scale(scale: 0.000001))

to your detail view to replace the default fade-in transition.

Here is all of the code:

struct ContentView: View {
    let colors: [Color] = [.red, .orange, .green, .indigo]
    @Namespace var nameSpace
    @State var showDetailView = false
    @State var selectedColorIndex: Int? = nil
    
    var body: some View {
        ZStack {
            Color.blue
            Grid(horizontalSpacing: 0, verticalSpacing: 0) {
                ForEach(0..<2) { row in
                    GridRow {
                        ForEach(0..<2) { column in
                            let index = (row * 2) + column
                            colors[index]
                                .matchedGeometryEffect(id: index, in: nameSpace, isSource: !showDetailView)
                                .onTapGesture {
                                    withAnimation {
                                        selectedColorIndex = index
                                        showDetailView = true
                                    }
                                }
                                .zIndex(selectedColorIndex == index ? 1 :  0)
                        }
                    }
                }
            }
            if showDetailView {
                if let index = selectedColorIndex {
                    colors[index]
                        .matchedGeometryEffect(id: index, in: nameSpace)
                        .transition(.scale(scale: 0.000001))
                        .onTapGesture {
                            withAnimation {
                                showDetailView = false
                            }
                        }
                        .zIndex(1)
                }
            }
        }
        .frame(width: 300, height: 300)
    }
}