DEV Community

noah.k
noah.k

Posted on

LazyVStack item animation on appearance and disappearance

How can you fade out LazyVStack 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

Top comments (0)