Create a list in SwiftUI with sticky section headers

Raviraj Wadhwa
Evangelist Apps Blog
3 min readJan 11, 2023

Recently I was assigned a task to create a list that has multiple sections and each section has a `heading` and some items. Please refer to the below gif to understand the requirement.

Looking at the design I straightaway thought of using a List view. Because in List we can add Sections with custom cells and a header. And these headers are by default sticky. Similar to UITableView.

List {
Section {
// Here goes the items
} header: {
// Here goes the header
}

// Here we can add any no. of sections...

}

While implementing the List view I found out that by default it has row separators, section header separators, white background colour, and different row and header insets (screenshot attached).

So I started my research on customizing the List view and after some research, I found out that the List view is very tricky to customize. The properties of hiding row separators, section header separators, and changing background colour were unavailable for iOS 14. Yes, it was the client's requirement to support iOS14. Also, changing the header and row insets of the List view was extra work.

So I decided not to use List view and was looking for some other solution and I found that in the form of LazyVStack.

LazyVStack is a view that arranges its children in a line that grows vertically, creating items only as needed. The stack is “lazy,” in the sense that the stack view doesn’t create items until it needs to render them onscreen. Example:

ScrollView {
LazyVStack(alignment: .leading) {
ForEach(1...100, id: \.self) {
Text("Row \($0)")
}
}
}

The plus point of LazyVStack is that it comes with one more initializer where we can have pinned (sticky) views.

Please refer to this for more details:

https://developer.apple.com/documentation/swiftui/lazyvstack

https://developer.apple.com/documentation/swiftui/lazyvstack/init(alignment:spacing:pinnedviews:content:)

And this is how I have used it in my app.

    var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) {
// Lakes
Section {
ForEach(lakes, id: \.self) { landmark in
LandmarkCellView(landmarkName: landmark)
}
} header: {
ListHeaderView(title: "Lakes")
}

// Mountains
Section {
ForEach(mountains, id: \.self) { landmark in
LandmarkCellView(landmarkName: landmark)
}
} header: {
ListHeaderView(title: "Mountains")
}

// Rivers
Section {
ForEach(rivers, id: \.self) { landmark in
LandmarkCellView(landmarkName: landmark)
}
} header: {
ListHeaderView(title: "Rivers")
}
}
}
.padding(.horizontal, Constants.padding15)
}

As per the example, I embed LazyVStack in ScrollView. Then I have three Sections one for Lakes, one for Mountains, and one for Rivers. Each Section has its own header label and cells. And the result is as follows.

Thanks for your time. I hope it helps you.

--

--