I try to create timeline step using the following code.
import SwiftUI
struct ContentView: View {
var body: some View {
// Timeline Steps Container
VStack(alignment: .leading, spacing: 0) {
TimelineStep(icon: "checkmark.circle.fill", title: "Install & Set up", description: "You've successfully personalized your experience", isCompleted: true, isLast: false)
TimelineStep(icon: "lock.fill", title: "Today: Get Instant Access", description: "Access 50+ premium actions: professional PDF editing, files converter, and scanner", isCompleted: true, isLast: false)
TimelineStep(icon: "bell.fill", title: "Day 5: Trial Reminder", description: "We'll send you an email/notification that your trial is ending", isCompleted: false, isLast: false)
TimelineStep(icon: "star.fill", title: "Day 7: Trial Ends", description: "Your subscription will start on Apr 19", isCompleted: false, isLast: true)
Spacer().layoutPriority(1)
}
.padding(.vertical, 30)
}
}
// --- PLEASE REPLACE YOUR OLD TimelineStep WITH THIS ---
struct TimelineStep: View {
let icon: String
let title: String
let description: String
let isCompleted: Bool
let isLast: Bool
var body: some View {
HStack(alignment: .top, spacing: 20) {
// --- FIX #1: ICON AND LINE CONNECTION ---
// This VStack now has spacing set to 0 to remove the gap.
VStack(alignment: .center, spacing: 0) {
//Rectangle()
// .fill(isCompleted ? Color.blue : Color(UIColor.systemGray5))
// .frame(width: 2)
// .frame(maxHeight: .infinity)
ZStack {
Circle()
.fill(isCompleted ? Color.blue : Color(UIColor.systemGray5))
.frame(width: 40, height: 40)
Image(systemName: icon)
.foregroundColor(isCompleted ? .white : .gray)
.font(.title3)
}
if !isLast {
Rectangle()
.fill(isCompleted ? Color.blue : Color(UIColor.systemGray5))
.frame(width: 2)
}
}
.frame(width: 40) // Give the icon column a fixed width
.frame(maxHeight: .infinity) // 1. Expand the frame to fill the available height
.background(.red)
// --- FIX #2: TEXT WRAPPING ---
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
.fontWeight(.bold)
Text(description)
.font(.subheadline)
.foregroundColor(.gray)
.fixedSize(horizontal: false, vertical: true)
// The .lineLimit modifier has been removed to allow wrapping.
}
.padding(.bottom, 32)
.background(.green)
// This is crucial for the text to wrap correctly by taking available space.
.frame(maxWidth: .infinity, alignment: .leading)
}
.background(.yellow)
.padding(.horizontal, 30) // Add padding to the whole row
.padding(.bottom, isLast ? 0 : 0) // Control space between timeline steps
}
}
#Preview {
ContentView()
}
I am getting this output.
My expectation is
VStack
should grow same height as green VStack
. It doesn't.VStack
. It doesn't.Why is it so?
Thank you. I have worked together with AI for quite a while. Both of us still can't figure out why 🫣
It seems that the height of a row should be determined by the text with the green background. Then the progress indicator should adopt this height.
Instead of having two VStack
in parallel, the VStack
with the progress indicator can be shown as an overlay over the one that determines the height. Padding can be used to reserve space for this overlay. With this approach, the HStack
is not needed any more.
// TimelineStep
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
.fontWeight(.bold)
Text(description)
.font(.subheadline)
.foregroundStyle(.gray)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.bottom, 32)
.background(.green)
.padding(.leading, 40 + 20)
.overlay(alignment: .leading) {
VStack(alignment: .center, spacing: 0) {
ZStack {
Circle()
.fill(isCompleted ? Color.blue : Color(UIColor.systemGray5))
.frame(width: 40, height: 40)
Image(systemName: icon)
.foregroundColor(isCompleted ? .white : .gray)
.font(.title3)
}
if !isLast {
Rectangle()
.fill(isCompleted ? Color.blue : Color(UIColor.systemGray5))
.frame(width: 2)
}
}
.frame(maxHeight: .infinity, alignment: .top) // 1. Expand the frame to fill the available height
.background(.red)
}
.frame(maxWidth: .infinity, alignment: .leading)
.background(.yellow)
.padding(.horizontal, 30) // Add padding to the whole row
}
You were previously setting a frame with maxHeight: .infinity
on each row. Then you had a Spacer
with higher layout priority, which consumed all the space below the rows. It was this higher layout priority that was causing the strange heights for the progress indicators before.
The frame setting maxHeight
is still used for the overlay, but it doesn't have any impact on the row height any more. So the layout priority is no longer needed:
// Timeline Steps Container
VStack(alignment: .leading, spacing: 0) {
// ... TimelineSteps, as before
Spacer() //.layoutPriority(1) // 👈 layoutPriority not necessary
}
.padding(.vertical, 30)