The best way that I'm aware of to fix this problem is to get rid of if expanded
and unconditionally include the speaker rows, but put the speaker rows in a container with a frame height of 0 when the session is collapsed. Add a clipping modifier to the outer session view (the view that draws the rounded rect and shadow) to hide the speaker rows when the card is collapsed. Here's the result:

Here's my SessionView
code:
struct SessionView: View {
var session: Session
@Binding var expanded: Bool
var body: some View {
VStack(alignment: .leading, spacing: 0) {
VStack(alignment: .leading, spacing: 16) {
HStack {
VStack(alignment: .leading, spacing: 8) {
(
Text(session.startDate, style: .time)
+ Text(" - ")
+ Text(session.endDate, style: .time)
)
.font(.caption)
Text(session.title)
.font(.headline)
}
Spacer()
if session.canExpand {
Image(systemName: "chevron.down")
.rotationEffect(.degrees(expanded ? 180 : 360))
}
}
}
VStack(alignment: .leading, spacing: 16) {
Spacer().frame(height: 0)
ForEach(session.speakers) { speaker in
SpeakerRow(speaker: speaker)
}
}
.frame(height: expanded ? nil : 0, alignment: .top)
}
.padding()
.contentShape(shape)
.clipShape(shape)
.onTapGesture {
if session.canExpand {
expanded.toggle()
}
}
.background {
shape
.fill(.white)
.padding(3)
.shadow(radius: 2, x: 0, y: 1)
}
}
private var shape: some Shape {
RoundedRectangle(cornerRadius: 10, style: .continuous)
}
}
These are the main things to note in my code:
I unconditionally include the speaker rows.
I wrap the speaker rows in their own VStack
. That VStack
has a frame
modifier with height zero if the session is not expanded.
I apply a clipShape
modifier to the outer VStack
so that the speaker rows are clipped when the session is collapsed.
Here's the rest of the code, for experimentation:
struct Speaker: Identifiable {
var name: String
var image: String
var id: String { name }
}
struct Session: Identifiable {
var startDate: Date
var endDate: Date
var title: String
var speakers: [Speaker]
var canExpand: Bool { !speakers.isEmpty }
var id: String { title }
}
struct Avatar: View {
var name: String
var size: CGFloat
var imagePath: String
var body: some View {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: size, height: size)
}
}
struct SpeakerRow: View {
var speaker: Speaker
var body: some View {
HStack {
Avatar(name: speaker.name, size: 24, imagePath: speaker.image)
Text(speaker.name)
}
}
}
struct AgendaView: View {
var sessions: [Session]
@State var expandedSessionId: String? = nil
var body: some View {
ScrollView {
VStack {
ForEach(sessions) { session in
SessionView(
session: session,
expanded: .init(
get: { expandedSessionId == session.id },
set: { expand in
if expand {
expandedSessionId = session.id
} else if expandedSessionId == session.id {
expandedSessionId = nil
}
}
)
)
}
}
.animation(.easeInOut(duration: 1), value: expandedSessionId)
}
.padding()
}
}
#Preview {
AgendaView(sessions: [
.init(
startDate: .init(timeIntervalSinceReferenceDate: 9000),
endDate: .init(timeIntervalSinceReferenceDate: 9900),
title: "Keynote",
speakers: [
.init(name: "Tim Cook", image: "tim.jpg"),
.init(name: "Johnny Appleseed", image: "apple.jpg"),
]
),
.init(
startDate: .init(timeIntervalSince1970: 10000),
endDate: .init(timeIntervalSince1970: 10900),
title: "Gettysburg Address",
speakers: [
.init(name: "Abraham Lincoln", image: "abe.jpg"),
.init(name: "Abe's Beard", image: "beard.jpg"),
]
),
.init(
startDate: .init(timeIntervalSince1970: 11000),
endDate: .init(timeIntervalSince1970: 11900),
title: "Ted Talk",
speakers: [
.init(name: "Ted Lasso", image: "lasso.jpg"),
.init(name: "Ted ‘Theodore’ Logan", image: "ted.jpg"),
]
)
])
}