I’m trying to animate a row expansion inside a List when the user taps a note icon. I’m wrapping the state change in withAnimation, but the note appears with a poor animation and the rows below are pushed down without animation (they “jump” abruptly). How can I animate the expansion properly, including the layout shift of the rows below?
Here is a minimal working example that reproduces the behavior (copy into a new SwiftUI project and run):
import SwiftUI
@main
struct ExpansionApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct HistoryItem: Identifiable, Hashable {
let id: UUID
let title: String
let note: String?
}
// MARK: - View
struct ContentView: View {
// Keeps the sample aligned with the real screen behavior for note expansion.
@State private var expandedNoteItems: Set = []
private let items: [HistoryItem] = [
HistoryItem(id: UUID(), title: "Client A - Morning shift", note: "Short note for entry 1."),
HistoryItem(id: UUID(), title: "Client B - Field visit", note: "Longer note for entry 2 so the row expands more than the others."),
HistoryItem(id: UUID(), title: "Client C - Training", note: nil),
HistoryItem(id: UUID(), title: "Client D - Support", note: "Another note for entry 4."),
HistoryItem(id: UUID(), title: "Client E - Wrap up", note: "Final note for entry 5.")
]
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))
}
}
.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)
}
}
}
}
// MARK: - Preview
#Preview {
ContentView()
}