Summary
The issue of SwiftUI List row expansion not animating smoothly, with items below jumping abruptly, is a common problem encountered when trying to animate a row expansion inside a List. The root cause of this issue lies in the way SwiftUI handles animations and layout changes.
Root Cause
The root cause of this issue is due to the following reasons:
- Incorrect animation handling: The
withAnimationblock is not properly handling the animation of the row expansion. - Layout changes: The layout changes caused by the expansion of the row are not being animated smoothly.
Why This Happens in Real Systems
This issue occurs in real systems because:
- Complexity of SwiftUI: SwiftUI is a complex framework that handles many aspects of the user interface, including animations and layout changes.
- Default animation behavior: The default animation behavior of SwiftUI can sometimes lead to abrupt changes in the layout, rather than smooth animations.
Real-World Impact
The real-world impact of this issue is:
- Poor user experience: The abrupt jumping of items below the expanded row can be jarring and disrupt the user’s experience.
- Loss of engagement: A poor user experience can lead to a loss of engagement and a negative perception of the app.
Example or Code
struct ContentView: View {
@State private var expandedNoteItems: Set = []
private let items: [HistoryItem] = [
// ...
]
var body: some View {
NavigationStack {
List {
ForEach(items, id: \.id) { item in
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 12) {
Text(item.title)
.font(.headline)
Spacer()
if item.note != nil {
Button {
toggleNote(for: item.id)
} label: {
Image(systemName: "note.text")
.imageScale(.medium)
}
.buttonStyle(.plain)
}
}
if let note = item.note, expandedNoteItems.contains(item.id) {
Text(note)
.font(.subheadline)
.foregroundStyle(.secondary)
.padding(10)
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
.animation(.easeInOut(duration: 0.5), value: expandedNoteItems)
}
}
.padding(.vertical, 8)
}
}
.listStyle(.plain)
.navigationTitle("History")
}
}
private func toggleNote(for id: UUID) {
withAnimation {
if expandedNoteItems.contains(id) {
expandedNoteItems.remove(id)
} else {
expandedNoteItems.insert(id)
}
}
}
}
How Senior Engineers Fix It
Senior engineers fix this issue by:
- Using explicit animations: Using explicit animations, such as
.animation(.easeInOut(duration: 0.5), value: expandedNoteItems), to control the animation of the row expansion. - Handling layout changes: Handling layout changes smoothly, by using techniques such as animating the height of the row, or using a transition to animate the appearance and disappearance of the note.
Why Juniors Miss It
Juniors may miss this issue because:
- Lack of experience: Juniors may not have enough experience with SwiftUI and animations to recognize the issue.
- Overreliance on default behavior: Juniors may rely too heavily on the default behavior of SwiftUI, rather than taking control of the animation and layout changes themselves. Key takeaways include the importance of explicit animations and smooth layout changes in creating a good user experience.