DEV Community

noah.k
noah.k

Posted on • Edited on

LazyVStack Animation on Appearance and Disappearance

How can you fade out LazyVStack or LazyHStack item when it disappears, without animating when it appears?
In JetPack Compose, I'd use Modifier.animateItem() (previously Modifier.animateItemPlacement()), but I didn't know how in SwiftUI, so I tried several patterns.

Implicit Animation

LazyVStack.animation()

LazyVStack.animation

  • animates both on appearance and disappearance
  • items fade in or out
struct ContentView: View {
    @State private var numbers = Array(0..<3)

    var body: some View {
        VStack {
            LazyVStack {
                Button("Append") {
                    let lastValue = numbers.last ?? -1
                    numbers.append(lastValue+1)
                }
                .padding()

                ForEach(numbers, id: \.self) { number in
                    Text(String(number))
                        .padding()
                        .background(.mint)
                        .onTapGesture {
                            if let index = numbers.firstIndex(where: { $0 == number }) {
                                numbers.remove(at: index)
                            }
                        }
                }
            }
            .animation(.easeIn(duration: 1.0), value: numbers)

            Spacer()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Item.animation()

Item.animation

  • animates only on disappearance, not on appearance
  • items don't fade out
struct ContentView: View {
    @State private var numbers = Array(0..<3)

    var body: some View {
        VStack {
            LazyVStack {
                Button("Append") {
                    let lastValue = numbers.last ?? -1
                    numbers.append(lastValue+1)
                }
                .padding()

                ForEach(numbers, id: \.self) { number in
                    Text(String(number))
                        .padding()
                        .background(.mint)
                        .onTapGesture {
                            if let index = numbers.firstIndex(where: { $0 == number }) {
                                numbers.remove(at: index)
                            }
                        }
                        .animation(.easeIn(duration: 1.0), value: numbers)
                }
            }

            Spacer()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Explicit Animation

Explicit Animation

Standard Code

  • this code is enough for most cases
  • animates only on disappearance, not on appearance
  • items fade out
struct ContentView: View {
    @State private var numbers = Array(0..<3)

    var body: some View {
        VStack {
            LazyVStack {
                Button("Append") {
                    let lastValue = numbers.last ?? -1
                    numbers.append(lastValue+1)
                }
                .padding()

                ForEach(numbers, id: \.self) { number in
                    Text(String(number))
                        .padding()
                        .background(.mint)
                        .transition(.opacity)
                        .onTapGesture {
                            if let index = numbers.firstIndex(where: { $0 == number }) {
                                _ = withAnimation(.easeIn(duration: 1.0)) {
                                    numbers.remove(at: index)
                                }
                            }
                        }
                }
            }

            Spacer()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

By Keeping Previous Value

  • In case you don't have access to update the value to monitor, you need to keep the previous and current value
  • animates only on disappearance, not on appearance
  • items fade out
struct ContentView: View {
    @State private var numbers = Array(0..<3)

    var body: some View {
        VStack {
            Button("Append") {
                let lastValue = numbers.last ?? -1
                numbers.append(lastValue+1)
            }
            .padding()

            ListView(
                numbers: $numbers,
                onTapNumber: { number in
                    if let index = numbers.firstIndex(where: { $0 == number }) {
                        numbers.remove(at: index)
                    }
                }
            )

            Spacer()
        }
    }
}

struct ListView: View {
    @Binding var newNumbers: [Int]
    @State private var numbers: [Int]
    let onTapNumber: (Int) -> Void

    init(numbers: Binding<[Int]>, onTapNumber: @escaping (Int) -> Void) {
        self._newNumbers = numbers
        self.numbers = numbers.wrappedValue
        self.onTapNumber = onTapNumber
    }

    var body: some View {
        LazyVStack {
            ForEach(numbers, id: \.self) { number in
                Text(String(number))
                    .padding()
                    .background(.mint)
                    .transition(.opacity)
                    .onTapGesture { onTapNumber(number) }
            }
        }
        .onChange(of: newNumbers) {
            if newNumbers.count < numbers.count {
                withAnimation(.easeInOut(duration: 1.0)) {
                    // Code for a case where only one item can be removed at once
                    if let index = numbers.indices.first(where: {
                        numbers[$0] != newNumbers[safe: $0]
                    }) {
                        numbers.remove(at: index)
                    }
                }
            } else {
                numbers = newNumbers
            }
        }
    }
}

extension Collection {
    subscript (safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}
Enter fullscreen mode Exit fullscreen mode

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more