Isaiah
Isaiah

Reputation: 1902

How to transition subviews in SwiftUI using matchedGeometryEffect?

I have a number of SwapItem structs, each with a child SwapItemChild. Then, using a ForEach of SwiftUI, I would like to display the name of each SwapItem, called the item view, containing also a circle in the color of its respective SwapItemChild, called the child view. Subsequently, I would like to swap the children of two items, and have the respective child views change places animated. This was inspired by other examples of this effect by this extensive tutorial, but not specifically the children view swapping.

I attempt to do so using a matchedGeometryEffect identifying each child view by the id of the respective SwapItemChild. However, this leads to a jumpy animation, where only the top child view moves down, whereas the bottom child view instantaneously jumps to the top.

The functional example code is as follows.

// MARK: - Model
struct SwapItem: Identifiable {
    let id = UUID()
    let name: String
    var child: SwapItemChild
}

struct SwapItemChild: Identifiable {
    let id = UUID()
    let color: Color
}

class SwapItemStore: ObservableObject {
    @Published private(set) var items = [SwapItem(name: "Task 1", child: SwapItemChild(color: .red)),
                                         SwapItem(name: "Task 2", child: SwapItemChild(color: .orange))]
    
    func swapOuterChildren(){
        let tmpChild = items[0].child
        items[0].child = items[1].child
        items[1].child = tmpChild
    }
}

// MARK: - View
struct SwapTestView: View {
    @StateObject private var swapItemStore = SwapItemStore()
    @Namespace private var SwapViewNS
    
    var body: some View {
        VStack(spacing: 50.0) {
            Button(action: swapItemStore.swapOuterChildren){
                Text("Swap outer children")
                    .font(.title)
            }
            
            VStack(spacing: 150.0) {
                ForEach(swapItemStore.items){ item in
                    SwapTestItemView(item: item, ns: SwapViewNS)
                }
            }
        }
    }
}

struct SwapTestItemView: View {
    let item: SwapItem
    let ns: Namespace.ID
    
    var body: some View {
        HStack {
            Circle()
                .fill(item.child.color)
                .frame(width: 100, height: 100)
                .matchedGeometryEffect(id: item.child.id, in: ns)
                .animation(.spring())
            
            Text(item.name)
        }
    }
}

What is the correct implementation of matchedGeometryEffect to have these child views swapping places seamlessly?

Upvotes: 4

Views: 1066

Answers (1)

fdelsert
fdelsert

Reputation: 808

I have already encountered this kind of problem, try this :

ForEach(swapItemStore.items, id: \.self.child.id)

swap animation

Another way :

struct SwapItem: Identifiable, Hashable {
    let id = UUID()
    let name: String
    var child: SwapItemChild
}

struct SwapItemChild: Identifiable, Hashable {
    let id = UUID()
    let color: Color
}

with :

ForEach(swapItemStore.items, id: \.self)

See : https://www.hackingwithswift.com/books/ios-swiftui/why-does-self-work-for-foreach

Upvotes: 3

Related Questions