I am trying to animate movement between LazyVGrid views. I have two arrays of small Circle shapes (i.e. "dots") arranged in two different LazyVGrid Views (one on the left, one on the right). When the user taps a button, I want to animate each of those dots sliding from their original spot into their spot in a third LazyVGrid with all of them combined.
As if you had autonomous, but obedient, marbles arranged in grids in two places on a table, and when you command them to do so, they roll together to form a new grid in the center of the table (but simpler — no collisions, etc).
After many attempts, the following code is essentially my best attempt so far (though it looks dumb-ish out of context). The "Fill" button populates the original but it still just animates like the original grids of dots fading out and new dots fading in, and then more new dots fading in as the first set of new dots slides up.
Any tips on implementing this type of behavior?
import SwiftUI
struct CT2: View {
@StateObject private var viewModel = DotManager()
var body: some View {
VStack {
ZStack {
HStack {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 20), count: 5), content: {
ForEach(viewModel.blueDots) { dot in
dot.dot
.frame(width: 20, height: 20)
.foregroundColor(.blue)
}
})
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 20), count: 5), content: {
ForEach(viewModel.redDots) { dot in
dot.dot
.frame(width: 20, height: 20)
.foregroundColor(.red)
}
})
}
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 20), count: 5), content: {
ForEach(viewModel.combinedDots) { dot in
dot.dot
.frame(width: 20, height: 20)
.foregroundColor(.purple)
}
})
}
}
HStack {
Button(action: {
viewModel.addDots()
}, label: {
Text("Fill")
})
.padding()
.foregroundColor(.primary)
.background(Color(UIColor.systemFill))
Button(action: {
viewModel.combineDots()
}, label: {
Text("Combine dots")
})
.padding()
.foregroundColor(.primary)
.background(Color(UIColor.systemFill))
}
}
}
enum DotType {
case blue, red, combined
}
class Dot: Identifiable {
let id = UUID()
let dot = Circle()
}
class DotManager: ObservableObject {
@Published var blueDots: [Dot] = []
@Published var redDots: [Dot] = []
@Published var combinedDots: [Dot] = []
func addDots() {
for _ in 0..<17 {
let newDot = Dot()
blueDots.append(newDot)
}
for _ in 17..<28 {
let newDot = Dot()
redDots.append(newDot)
}
}
func combineDots() {
for _ in 0..<blueDots.count {
withAnimation(.easeInOut) {
if let movedDot = blueDots.first {
blueDots.removeFirst()
combinedDots.append(movedDot)
}
}
}
for _ in 0..<redDots.count {
withAnimation(.easeInOut) {
if let movedDot = redDots.first {
redDots.removeFirst()
combinedDots.append(movedDot)
}
}
}
}
}
2
Answers
To achieve the desired effect with your views, you can make use of @Namespace and .matchedGeometryEffect
The modified code is as below.
and inside each ForEach loops (you have three ForEach here), add .matchedGeometryEffect.
As I was suggesting in a comment,
.matchedGeometryEffect
can be used for this.In order for the dots to appear as if they are moving smoothly from one position to another, it helps if both the "before" and "after" versions can exist at the same time. When this is not the case, I found that the dots would move, but the before and after positions were not properly synchronized. This means, you can see how one set of dots fades out as they move off while the other set fades in, so the effect is a bit lost.
In the updated example below, the dots are added to the combined array at the same time as they are added to the blue and red arrays. All dots are also displayed at all times, but the visibility is controlled using a boolean state variable that determines the opacity of the dots. The same variable is also used to determine the
isSource
flag for the.matchedGeometryEffect
.