I have a ForEach where I add and remove items, but I am experiencing an animation issue with LazyVStack.
LazyVStack reuses views, so when I remove an item and then add a new one at the same index, SwiftUI sees it as the same view and does not play the insertion animation.
To force animations, I updated LazyVStack’s ID (lazyID), but that causes all items to re-render, which is inefficient.
I want animations to work properly when adding/removing items without triggering a full re-render of existing views.
The Root Problem:
LazyVStack optimizes rendering by reusing views. When an item is removed, SwiftUI does not fully deallocate it immediately. When a new item is inserted at the same index, SwiftUI reuses the old view, skipping the animation.
What I Need:
A way to make SwiftUI recognize an insertion as a true "new" item. Avoid using .id(UUID()) on LazyVStack (which forces re-rendering of all views). Prevent LazyVStack from thinking it’s reusing an old view when adding a new item at a previously removed index.
macOS:[macos 15.2 (24C101), xcode 16.2]
import SwiftUI
struct ContentView: View {
@State private var items: [ItemType] = []
@State private var lazyID: UUID = UUID()
var body: some View {
VStack {
ScrollView {
LazyVStack(spacing: 5.0) {
ForEach(items) { item in
CircleView(item: item)
.transition(
.asymmetric(
insertion: .move(edge: .top),
removal: .move(edge: .top)
)
)
}
}
.id(lazyID)
.animation(Animation.linear , value: items)
}
.padding()
Spacer()
HStack {
Button("Append New Item") {
let newItem = ItemType(value: items.count + 1)
items.append(newItem)
}
Button("Remove last Item") {
if let last = items.popLast() {
print("Removed:", last.value)
} else {
print("Array is empty!")
}
// This will allow the animation to happen when adding a new item, but at the cost of re-rendering all views.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(1000)) {
// lazyID = UUID()
}
}
}
}
.padding()
}
}
struct CircleView: View, Equatable {
let item: ItemType
var body: some View {
print("CircleView called for: " + String(describing: item.value))
return Circle()
.fill(Color.red)
.frame(width: 50.0, height: 50.0)
.overlay(Circle().stroke(lineWidth: 1.0))
.overlay(Text("\(item.value)").foregroundStyle(.white))
}
static func == (lhs: Self, rhs: Self) -> Bool {
(lhs.item.id == rhs.item.id)
}
}
struct ItemType: Identifiable, Equatable {
let id: UUID = UUID()
let value: Int
static func == (lhs: Self, rhs: Self) -> Bool {
(lhs.id == rhs.id)
}
}

