It is generally difficult to animate a change in height for a List row smoothly. However, a possible workaround is to use a DisclosureGroup.
When a DisclosureGroup with native styling is used in a List, it navigates quite smoothly. The way it works is actually to add a new row to the list when expanded. To use your own styling, a custom DisclosureGroupStyle can be used. By using the same technique of adding an extra row for the expanded content, the animation works quite smoothly.
Some notes:
- Tracking the expanded state can be handled by the
DisclosureGroup. - The function
makeBodyof the custom style should return two rows when the content is expanded. This is done by not using aVStackas outer container for the content. - The vertical spacing for the expanded content can be reduced by applying custom
.listRowInsets. - The list row separator above the expanded note can be hidden.
- The alignment of the separator below the expanded note can be controlled using an
.alignmentGuide.
Here is how your example can be adapted to work this way:
struct MyDisclosureGroupStyle: DisclosureGroupStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(spacing: 12) {
configuration.label
Spacer()
Button {
withAnimation {
configuration.isExpanded.toggle()
}
} label: {
Image(systemName: "note.text")
.imageScale(.medium)
}
.buttonStyle(.plain)
}
.padding(.vertical, 8)
if configuration.isExpanded {
configuration.content
.listRowInsets(.init(top: 0, leading: 16, bottom: 10, trailing: 16))
.padding(.bottom, 8)
.listRowSeparator(.hidden, edges: .top)
.alignmentGuide(.listRowSeparatorLeading) { dim in
0
}
}
}
}
struct ContentView: View {
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) { item in
if let note = item.note {
DisclosureGroup {
Text(note)
.font(.subheadline)
.foregroundStyle(.secondary)
.padding(10)
.background(.background.secondary, in: .rect(cornerRadius: 8))
} label: {
Text(item.title)
.font(.headline)
}
.disclosureGroupStyle(MyDisclosureGroupStyle())
} else {
Text(item.title)
.font(.headline)
.padding(.vertical, 8)
}
}
}
.listStyle(.plain)
.navigationTitle("History")
}
}
}
