Ashley Mills
Ashley Mills

Reputation: 53231

Issue using matchedGeometryEffect animation in ScrollView

Animating Views between a LazyVGrid and an HStack with another view in between them (in this case, a Button), using matchedGeometryEffect, works great:

enter image description here

Note how the animating views move above the Done button.

However, when the views are contained within a ScrollView, the animating views now move behind the intermediate view:

enter image description here

I've tried setting the zIndex of the ScrollViews to > 0 (or more) but this doesn't seem to change anything.

Any thoughts on how to fix this?

Person

struct Person: Identifiable, Equatable {
    var id: String { name }
    let name: String
    var image: Image { Image(name) }
    
    static var all: [Person] {
        ["Joe", "Kamala", "Donald", "Mike"].map(Person.init)
    }
}

ContentView

struct ContentView: View {

    @State var people: [Person]
    @State private var selectedPeople: [Person] = []
    @Namespace var namespace

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            ScrollView(.horizontal) {
                SelectedPeopleView(people: $selectedPeople, namespace: namespace) { person in
                    withAnimation(.easeOut(duration: 1)) {
                        selectPerson(person)
                    }
                }
                .background(Color.orange)
            }
            doneButton()
            ScrollView(.vertical) {
                PeopleView(people: people, namespace: namespace) { person in
                    withAnimation(.easeOut(duration: 1)) {
                        deselectPerson(person)
                    }
                }                
            }
            Spacer()
        }
        .padding()
    }

    func selectPerson(_ person: Person) {
        _ = selectedPeople.firstIndex(of: person).map { selectedPeople.remove(at: $0)}
        people.append(person)
    }

    func deselectPerson(_ person: Person) {
        _ = people.firstIndex(of: person).map { people.remove(at: $0)}
        selectedPeople.append(person)
    }

    func doneButton() -> some View {
        Button("Done") {
        }
        .font(.title2)
        .accentColor(.white)
        .frame(maxWidth: .infinity)
        .padding()
        .background(Color.gray)
    }
}

SelectedPeopleView

struct SelectedPeopleView: View {

    @Binding var people: [Person]
    let namespace: Namespace.ID
    let didSelect: (Person) -> Void

    var body: some View {
        HStack {
            ForEach(people) { person in
                Button(action: { didSelect(person) } ) {
                    Text(person.name)
                        .padding(10)
                        .background(Color.yellow.cornerRadius(6))
                        .foregroundColor(.black)
                        .matchedGeometryEffect(id: person.id, in: namespace)
                }
            }
        }
        .frame(height: 80)
    }
}

PeopleView

struct PeopleView: View {

    let people: [Person]
    let namespace: Namespace.ID
    let didSelect: (Person) -> Void

    let columns: [GridItem] = Array(repeating: .init(.flexible(minimum: .leastNormalMagnitude, maximum: .greatestFiniteMagnitude)), count: 2)

    var body: some View {
        LazyVGrid(columns: columns) {
            ForEach(people) { person in
                Button(action: { didSelect(person) }) {
                    person.image
                        .resizable()
                        .scaledToFill()
                        .layoutPriority(-1)
                        .clipped()
                        .aspectRatio(1, contentMode: .fit)
                        .cornerRadius(6)
                }
                .zIndex(zIndex(for: person))
                .matchedGeometryEffect(id: person.id, in: namespace)
            }
        }
    }

    func zIndex(for person: Person) -> Double {
        Double(people.firstIndex(of: person)!)
    }
}

Upvotes: 11

Views: 1361

Answers (2)

Arnaud
Arnaud

Reputation: 17834

Starting with iOS17, you can use

.scrollClipDisabled(true)

Upvotes: 1

Asperi
Asperi

Reputation: 258541

This looks like a bug in SwiftUI, because even if you put Color.clear of any height in place of and instead of your doneButton (or even .padding of some height for bottom ScrollView) the effect will be the same.

demo

As it is seen from view hierarchy there is nothing in between two ScrollView and rendering of images is performed in one single background view

demo2

Upvotes: 4

Related Questions