2

Recently ran into an issue trying to perform Hero animation using matchedGeometryEffect in SwiftUI. My issue is that setting id for matchedGeometryEffect effect dynamically isn't working as expected.

This is what I have so far:

import SwiftUI struct HeroAnimationTest: View { let items: [Item] = [.init(id: 1), .init(id: 2), .init(id: 3), .init(id: 4)] @State var selectedItemInRowIndex: Int? = nil @Namespace var namespace var body: some View { List { ForEach(items, id: \.id) { item in ItemListRow(namespace: namespace, item: item) { tappedItem in withAnimation { selectedItemInRowIndex = tappedItem.id } } } .listRowSeparator(.hidden) .listRowBackground(Color.clear) .listRowInsets(EdgeInsets(h: 16, v: 8)) } .animation(.spring(), value: selectedItemInRowIndex) .scrollIndicators(.never) .listStyle(.plain) .overlay { if selectedItemInRowIndex != nil { largeGreen } } } var largeGreen: some View { ZStack { Color.black .onTapGesture { withAnimation { selectedItemInRowIndex = nil } } Color.green .frame(width: 200, height: 400) .matchedGeometryEffect(id: selectedItemInRowIndex, in: namespace) Text("ID -> \(selectedItemInRowIndex ?? 0)") } } } struct HeroAnimationTest_Previews: PreviewProvider { static var previews: some View { HeroAnimationTest() } } struct Item { let id: Int } struct ItemListRow: View { @State var enlargeElement = false let namespace: Namespace.ID let item: Item let onGreenTap: (Item) -> Void var body: some View { HStack { Text("ID -> \(item.id)") VStack { Color.green } .frame(width: 100, height: 40) .matchedGeometryEffect(id: item.id, in: namespace) .onTapGesture { onGreenTap(item) } VStack { Color.yellow } .frame(width: 100, height: 40) } } } 

Current result:

enter image description here

I tried to hard-code id for largeGreen inside .matchedGeometryEffect(id: 3, in: namespace) to check if animation would work, and it does:

enter image description here

Animation with hard-coded id is the expected result, but obviously it's only working for the 3rd row. Is it even possible to achieve this effect for a green container in every row?

I'd really appreciate if anyone could take a look and give me some hint of what I'm missing here. I've been looking at this for a few hours now, but still can't figure out what went wrong.

1 Answer 1

1

Okay, I finally figured it out. It turns out that in order for matchedGeometryEffect to work, its id needs to be unwrapped, assigning optional just won't work.

So, what I did was:

  1. Extracted largeGreen to a ExpandedLargeGreen with selectedItemIdx & show parameters with @Binding property wrapper, and namespace.
  2. And then inside .overlay closure within HeroAnimationTest unwrapped optional Binding selectedItemInRowIndex. In case it's not nil and show is set to true ExpandedLargeGreen will be displayed with proper animation.
  3. Also set animation value to show instead of selectedItemInRowIndex.

Full code:

struct HeroAnimationTest: View { let items: [Item] = [.init(id: 1), .init(id: 2), .init(id: 3), .init(id: 4)] @State var selectedItemInRowIndex: Int? = nil @State var show = false @Namespace var namespace var body: some View { ZStack { List { ForEach(items, id: \.id) { item in ItemListRow(namespace: namespace, item: item) { tappedItem in withAnimation { selectedItemInRowIndex = tappedItem.id show.toggle() } } } .listRowSeparator(.hidden) .listRowBackground(Color.clear) .listRowInsets(EdgeInsets(h: 16, v: 8)) } .animation(.spring(), value: show) .scrollIndicators(.never) .listStyle(.plain) } .overlay { if let selectedIdx = Binding($selectedItemInRowIndex), show { ExpandedLargeGreen(selectedItemIdx: selectedIdx, show: $show, namespace: namespace) } } } } struct ExpandedLargeGreen: View { @Binding var selectedItemIdx: Int @Binding var show: Bool var namespace: Namespace.ID var body: some View { ZStack { Color.black .onTapGesture { withAnimation { show.toggle() } } Color.green .frame(width: 200, height: 400) .matchedGeometryEffect(id: selectedItemIdx, in: namespace) Text("ID -> \(selectedItemIdx)") } } } struct HeroAnimationTest_Previews: PreviewProvider { static var previews: some View { HeroAnimationTest() } } struct Item { let id: Int } struct ItemListRow: View { @State var enlargeElement = false let namespace: Namespace.ID let item: Item let onGreenTap: (Item) -> Void var body: some View { HStack { Text("ID -> \(item.id)") VStack { Color.green } .frame(width: 100, height: 40) .matchedGeometryEffect(id: item.id, in: namespace) .onTapGesture { onGreenTap(item) } VStack { Color.yellow } .frame(width: 100, height: 40) } } } 

Final result:

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.