How to generalise a List-like view that shows different content depending on the row number?

61 views Asked by At

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:

enter image description here

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 ListStyles.

0

There are 0 answers