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()
- 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()
}
}
}
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()
}
}
}
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()
}
}
}
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
}
}
Top comments (0)