skip to Main Content

I’m trying to use the matched geometry effect on a list and it is working like expected at least for the animation to the detail view.
The problem is that the animation back from the detail view to list list cell seems to be behind the list.

enter image description here

How can I get this working properly so that the detail view will shrink back to a list cell?
Here’s the code for my cell:

struct Cell: View {

var exercise: Exercise
var index: Int
var namespace: Namespace.ID

var body: some View {
    VStack {
        Text(exercise.title)
            .matchedGeometryEffect(id: exercise.id, in: namespace)
            .font(.title)
            .foregroundColor(.white)
            .frame(maxWidth: .infinity, alignment: .leading)
            .frame(height: 100)
    }
    .padding(.horizontal)
    .padding(.top)
    .padding(.bottom, 4)
    .background(
        RoundedRectangle(cornerRadius: 5, style: .continuous)
            .matchedGeometryEffect(id: exercise.id + "background", in: namespace)
            .background(Color.clear)
            .foregroundColor(Color.gray)
    )
    .clipped()
}
}

This is the list itself:

struct ListView: View {

var testData: [Exercise]
var namespace: Namespace.ID
@Binding var tappedCellIndex: Int?

var body: some View {
    ScrollView {
        LazyVStack {
            ForEach(testData.indices) { index in
                Cell(exercise: testData[index], index: index, namespace: namespace)
                .onTapGesture {
                    withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
                        self.tappedCellIndex = index
                    }
                }
            }
        }
    }
}
}

This is the Content View:

struct ContentView: View {
let testData = [Exercise(title: "Hallo"), Exercise(title: "Bankdrücken"), Exercise(title: "Squats"), Exercise(title: "Seitheben"), Exercise(title: "Klimmzüge"), Exercise(title: "Bizepscurls"), Exercise(title: "Dips"), Exercise(title: "Aufroller"), Exercise(title: "Muscle"), Exercise(title: "Dragon Flies"), Exercise(title: "Hallo"), Exercise(title: "Bankdrücken"), Exercise(title: "Squats"), Exercise(title: "Seitheben"), Exercise(title: "Klimmzüge"), Exercise(title: "Bizepscurls"), Exercise(title: "Dips"), Exercise(title: "Aufroller"), Exercise(title: "Muscle"), Exercise(title: "Dragon Flies")]

@State var tappedCellIndex: Int? = nil
@Namespace var namespace

var body: some View {
    ZStack {
        VStack {
            ListView(testData: testData, namespace: namespace, tappedCellIndex: $tappedCellIndex)
                .opacity(tappedCellIndex == nil ? 1 : 0)
                .transition(.scale(scale: 1))
        }
        
        if let tappedCellIndex = tappedCellIndex {
            DetailView(exercise: testData[tappedCellIndex], selectedIndex: $tappedCellIndex, namespace: namespace)
                .transition(.scale(scale: 1))
        }
    }
}
}

This is the detail view from where it should animate back:

struct DetailView: View {

var exercise: Exercise
@Binding var selectedIndex: Int?
var namespace: Namespace.ID

var body: some View {
    VStack(alignment: .leading) {
        HStack {
            Text(exercise.title)
                .matchedGeometryEffect(id: exercise.id, in: namespace)
                .font(.title)
                .frame(maxWidth: .infinity, alignment: .leading)
                .foregroundColor(Color.white)
                .padding(.top, 12)
            Spacer()
            Button {
                withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
                    selectedIndex = nil
                }
            } label: {
                Text("Klicke hier!")
            }
        }

        Spacer()
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .padding()
    .background(
        RoundedRectangle(cornerRadius: 30, style: .continuous)
            .matchedGeometryEffect(id: exercise.id + "background", in: namespace)
            .foregroundColor(Color.gray)
    )
}
}

And finally this is the model:

struct Exercise: Identifiable, Hashable, Equatable {
var id = UUID().uuidString
var title: String

static func == (lhs: Exercise, rhs: Exercise) -> Bool {
    return lhs.id == rhs.id
}

func hash(into hasher: inout Hasher) {
    hasher.combine(id)
}

}

sorry for that much code. I hope I added all that is needed.

btw: I also get the error "Multiple inserted views in matched geometry group". I think that is because I don’t hide the list when displaying the detail view.
I read different opinions on this here on stackoverflow and it seems to me thats its not in every case necessary to hide one view when another one is displayed and I maybe can ignore the error message.

I also cant hide the list because a reload would refresh the list and I will start at the top of the list again so the backward animation also won’t work.

I would appreciate any help

2

Answers


  1. Chosen as BEST ANSWER

    I found a solution for the problem. Just needed to set the zIndex for the DetailView like this..

    if let tappedCellIndex = tappedCellIndex {
            DetailView(exercise: testData[tappedCellIndex], selectedIndex: $tappedCellIndex, namespace: namespace)
                .zIndex(1)
                .transition(.scale(scale: 1))
        }
    

    Another thing that I needed to do is to remove the .clipped() from the Cell like ChrisR suggested. I didn't changed the order of the frame / matchedGeometryEffect modifier because that prevents a smooth animation in my case.


  2. I would definitely recommend to show an empty placeholder to prevent "multiple inserted views". Also the .frame(maxWidth, maxHeight) has to come before the .matchedGeometry, so the view can shrink or grow in the animation.

    enter image description here

    Here is the amended code, check the comments:

    struct Cell: View {
        
        var exercise: Exercise
        var index: Int
        var namespace: Namespace.ID
        
        var body: some View {
            VStack {
                Text(exercise.title)
                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) // moved + maxHeight!
                    .font(.title)
                    .foregroundColor(.white)
                    .matchedGeometryEffect(id: exercise.id, in: namespace)
            }
            .frame(height: 100) // moved
            .padding(.horizontal)
            .padding(.top)
            .padding(.bottom, 4)
            .background(
                RoundedRectangle(cornerRadius: 5, style: .continuous)
                    .matchedGeometryEffect(id: exercise.id + "background", in: namespace)
                    .background(Color.clear)
                    .foregroundColor(Color.gray)
            )
            //    .clipped()
        }
    }
    
    
    struct ListView: View {
        
        var testData: [Exercise]
        var namespace: Namespace.ID
        @Binding var tappedCellIndex: Int?
        
        var body: some View {
            ScrollView {
                LazyVStack {
                    ForEach(testData.indices) { index in
                        // check if tapped
                        if index != tappedCellIndex {
                            Cell(exercise: testData[index], index: index, namespace: namespace)
                                .onTapGesture {
                                    withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
                                        self.tappedCellIndex = index
                                    }
                                }
                        } else { // display empty space
                            Color.clear
                                .frame(height: 100)
                        }
                    }
                }
            }
        }
    }
    
    
    struct ContentView: View {
        let testData = [Exercise(title: "Hallo"), Exercise(title: "Bankdrücken"), Exercise(title: "Squats"), Exercise(title: "Seitheben"), Exercise(title: "Klimmzüge"), Exercise(title: "Bizepscurls"), Exercise(title: "Dips"), Exercise(title: "Aufroller"), Exercise(title: "Muscle"), Exercise(title: "Dragon Flies"), Exercise(title: "Hallo"), Exercise(title: "Bankdrücken"), Exercise(title: "Squats"), Exercise(title: "Seitheben"), Exercise(title: "Klimmzüge"), Exercise(title: "Bizepscurls"), Exercise(title: "Dips"), Exercise(title: "Aufroller"), Exercise(title: "Muscle"), Exercise(title: "Dragon Flies")]
        
        @State var tappedCellIndex: Int? = nil
        @Namespace var namespace
        
        var body: some View {
            ZStack {
                VStack {
                    ListView(testData: testData, namespace: namespace, tappedCellIndex: $tappedCellIndex)
                        .opacity(tappedCellIndex == nil ? 1 : 0)
                    //                .transition(.scale(scale: 1))
                }
                
                if let tappedCellIndex = tappedCellIndex {
                    DetailView(exercise: testData[tappedCellIndex], selectedIndex: $tappedCellIndex, namespace: namespace)
                    //                .transition(.scale(scale: 1))
                }
            }
        }
    }
    
    
    struct DetailView: View {
        
        var exercise: Exercise
        @Binding var selectedIndex: Int?
        var namespace: Namespace.ID
        
        var body: some View {
            VStack(alignment: .leading) {
                HStack {
                    Text(exercise.title)
                        .frame(maxWidth: .infinity, alignment: .leading) // moved
                        .matchedGeometryEffect(id: exercise.id, in: namespace)
                        .font(.title)
                        .foregroundColor(Color.white)
                        .padding(.top, 12)
                    Spacer()
                    Button {
                        withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
                            selectedIndex = nil
                        }
                    } label: {
                        Text("Klicke hier!")
                    }
                }
                
                Spacer()
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .padding()
            .background(
                RoundedRectangle(cornerRadius: 30, style: .continuous)
                    .matchedGeometryEffect(id: exercise.id + "background", in: namespace)
                    .foregroundColor(Color.gray)
            )
        }
    }
    
    
    struct Exercise: Identifiable, Hashable, Equatable {
        var id = UUID().uuidString
        var title: String
        
        static func == (lhs: Exercise, rhs: Exercise) -> Bool {
            return lhs.id == rhs.id
        }
        
        func hash(into hasher: inout Hasher) {
            hasher.combine(id)
        }
        
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search