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
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)
}
}