Suppose I am writing a SwiftUI view that can display a series of ordered steps. It displays the steps in a List
, and numbers them from 1 to n.
struct ContentView: View {
var body: some View {
List {
// example data:
ForEach(0..<10) { i in
Text("Content \(i)")
.listRowBackground(HStack {
Group {
if i == 0 {
StepIndicator(part: .top, stepNumber: i + 1)
} else if i == 9 {
StepIndicator(part: .bottom, stepNumber: i + 1)
} else {
StepIndicator(part: .middle, stepNumber: i + 1)
}
}.frame(width: 30)
Spacer()
})
}
.listStyle(.plain)
.listRowSeparator(.hidden)
.listRowInsets(.init(top: 0, leading: 36, bottom: 0, trailing: 0))
}
}
}
struct StepIndicator: View {
enum Part {
case top
case middle
case bottom
case dotOnly
}
let part: Part
let stepNumber: Int
var body: some View {
GeometryReader { geo in
ZStack {
switch part {
case .top:
Rectangle().fill(.blue)
.frame(width: 10, height: geo.size.height / 2)
.frame(maxHeight: .infinity, alignment: .bottom)
case .middle:
Rectangle().fill(.blue).frame(width: 10)
case .bottom:
Rectangle().fill(.blue).frame(width: 10)
.frame(width: 10, height: geo.size.height / 2)
.frame(maxHeight: .infinity, alignment: .top)
default:
EmptyView()
}
Circle().fill(.blue).frame(width: 20, height: 20)
Text("\(stepNumber)").font(.caption).foregroundStyle(.white)
}
}
}
}
Here is how it looks with some example content:
Now I want to generalise this, so that I can reuse it everywhere, and put it in a Swift Package for other people to use, and so on. Also, it should behave like a List
- each row (step) can be a different type of view. People can write "static" steps, or they can use ForEach
to create steps from a collection, and so on. The caller shouldn't need to worry about numbering the steps. For example, I would like to be able to rewrite ContentView.body
as:
StepsView {
ForEach(0..<10) { i in
Text("Content \(i)")
}
}
Another example:
StepsView {
Text("Step 1")
HStack {
Text("Step 2")
Image("something related to step 2")
}
ForEach(theRestOfTheSteps) { step in
StepView(step)
}
}
The problem is that I need to set a different listRowBackground
depending on the index of the row.
struct StepsView<Content: View>: View {
@ViewBuilder let content: () -> Content
var body: some View {
List {
content() // I can't "go inside" content!
.listStyle(.plain)
.listRowSeparator(.hidden)
.listRowInsets(.init(top: 0, leading: 36, bottom: 0, trailing: 0))
}
}
}
I wish I could do something like this in StepsView.body
, to "loop through" each view in content
(this is pseudocode, of course):
ForEach(content()) { row, i in
row
.listRowBackground(HStack {
Group {
if i == 0 {
StepIndicator(part: .top, stepNumber: i + 1)
} else if i == 9 {
StepIndicator(part: .bottom, stepNumber: i + 1)
} else {
StepIndicator(part: .middle, stepNumber: i + 1)
}
}.frame(width: 30)
Spacer()
})
}
How can I generalise this StepsView
?
I've seen this question, but the answer there cannot support a dynamic number of views.
I've always tried writing my own ListStyle
, but the requirements of ListStyle
involve a whole lot of underscore-prefixed types that have no public members. It seems like (correct me if I'm wrong) we are not supposed to make our own custom ListStyle
s.