最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swift - LazyVStack Reuses Views, Preventing Proper Animation on Re-Adding Items After Removal - Stack Overflow

programmeradmin1浏览0评论

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)
    }
}

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)
    }
}
Share Improve this question edited Feb 1 at 18:32 Mango asked Feb 1 at 15:17 MangoMango 556 bronze badges 4
  • The animations work as expected on iOS 18.1. Please show a minimal reproducible example. – Sweeper Commented Feb 1 at 17:27
  • The issue comes in macOS – Mango Commented Feb 1 at 17:46
  • I cannot reproduce the issue on macOS 15.1.1, either. – Sweeper Commented Feb 1 at 17:50
  • I don't know why we have different results in the same query – Mango Commented Feb 1 at 18:34
Add a comment  | 

2 Answers 2

Reset to default 1

In SwiftUI, Identity is everything. At its data model heart SwiftUI is a giant pile of nested collections that need diffing to detect identity changes and drive animation.

When you do this:

struct ItemType: Identifiable, Equatable {
    let id: UUID = UUID()
    let value: Int
}

You create a new identity for the same value on each instantiation.

so ItemType(value: 42) != ItemType(value: 42)

where you might expect the two structures to be equal as their values are the same.

Their identity, the thing that differentiates one ItemType from another is the value.

When you insert an item after e.g 3 it will always be 4 according to your business logic. However the 4 that you insert does not equal any 4 that may have existed previously as the id has now got a new value.

You (probably) don't introduce yourself with a new name each time you meet someone and that applies to data identity too.

In summary, decide what makes each instance of a data model object different from the other and don't just wedge let id: UUID = UUID() into every data model type to create difference for triggering SwiftUI drawing.

Your modified example, using value to drive the Identifiable conformance of ItemType provides correct animation.

import SwiftUI

struct CircleView: View {
    @State var item: ItemType
    
    var body: some View {
        return Circle()
            .fill(Color.red)
            .frame(width: 50.0, height: 50.0)
            .overlay(Text("\(item.value)").foregroundStyle(.white))
    }
}

struct ItemType: Equatable, Identifiable {
    var id: Int { value }
    let value: Int
}

struct ContentView: View {
    
    @State private var items: [ItemType] = []
    
    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)
                                )
                            )
                    }
                }
                .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") {
                    _ = items.popLast()
                }
                
            }
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

See https://developer.apple/videos/play/wwdc2021/10022/ for an expert description of what is going on.

It turn is out LazyVStack is literally lazy! I scavenged removed items ids to use the animation on new "Items".

import SwiftUI

struct ContentView: View {
    
    @State private var items: [ItemType] = [ItemType]()
    @State private var removedItemIDs: [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)
                                )
                            )
                        
                    }
                }
                .animation(Animation.linear , value: items)
            }
            .padding()
            
            Spacer()
            
            HStack {
                Button("Append New Item") {

                    if (removedItemIDs.isEmpty) {
                        let newItem: ItemType = ItemType(value: items.count + 1)
                        items.append(newItem)
                    }
                    else {
                        let oldID: UUID = removedItemIDs.removeFirst()
                        let newItem: ItemType = ItemType(id: oldID, value: items.count + 1)
                        items.append(newItem)
                    }
                    
                }
                
                Button("Remove last Item") {
                    if let last = items.popLast() {

                    if (!removedItemIDs.contains(where: { value in (value == last.id) })) {
                        removedItemIDs.append(last.id)
                    }
                        print("Removed:", last.value)

                    } else {
                        print("Array is empty!")
                    }

                }
                
            }
        }
        .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 == rhs.item)
    }
}

struct ItemType: Identifiable, Equatable {

    init(id: UUID, value: Int) {
        self.id = id
        self.value = value
    }
    
    init(value: Int) {
        self.id = UUID()
        self.value = value
    }
    
    let id: UUID
    let value: Int
    
    static func == (lhs: Self, rhs: Self) -> Bool {
        (lhs.id == rhs.id) && (lhs.value == rhs.value)
    }
}

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论