SwiftUI List Section Index Title
Reading time: 3 min
This recipe shows how to add section index with titles to a SwiftUI List. This will render a vertical list of custom shortcuts on the right-hand side of the list, allowing you to quickly navigate to any section by pressing or just moving your finger over it. The same functionality in UITableView
is implemented with sectionIndexTitles(for:)
and tableView(_:sectionForSectionIndexTitle:at:)
.
The end result looks like this:
This recipe relies on the touch enter/exit detector. Be sure to check it out!
The component is implemented as a ViewModifier
that attaches the section index to the right-hand side of the screen, above the list. (Actually, this works for any view wrapped in a ScrollViewReader
, but List
is the most common use-case.) It relies on a ScrollViewProxy to navigate to the target section and allows for custom views representing section shortcuts. Here's the code:
struct SectionIndex<ID, TitleContent>: ViewModifier where ID : Hashable, TitleContent : View {
let proxy: ScrollViewProxy
let sections: [ID]
@ViewBuilder let titleContent: (ID, Bool) -> TitleContent
@State private var selection: ID? = nil
func body(content: Content) -> some View {
ZStack {
content
// TouchEnterExitReader allows for tracking of finger movement over index shortcuts
TouchEnterExitReader(ID.self,
onEnter: { id in
selection = id
withAnimation {
proxy.scrollTo(id)
}
},
onExit: { id in
selection = nil
}) { touchEnterExitProxy in
HStack {
Spacer() // right-align the index
VStack { // the index itself
ForEach(sections, id: \.self) { section in
titleContent(section, selection == section)
.touchEnterExit(id: section, proxy: touchEnterExitProxy)
.onTapGesture {
withAnimation {
proxy.scrollTo(section)
}
}
}
}
}
}
}
}
}
extension View {
func sectionIndex<ID, TitleContent>(proxy: ScrollViewProxy,
sections: [ID],
@ViewBuilder titleContent: @escaping (ID, Bool) -> TitleContent) -> some View
where ID : Hashable, TitleContent : View {
self.modifier(SectionIndex(proxy: proxy, sections: sections, titleContent: titleContent))
}
}
If you're targeting SwiftUI 3 (iOS 15, macOS 12), you can improve the scrolling precision by supplying the anchor
parameter to scrollTo
:
proxy.scrollTo(section, anchor: .center) // only on SwiftUI 3
Alphabetical section index
The most common use case for the section index is to show the first letter of the section title as the shortcut, just like in the Contacts app:
extension View {
func firstLetterSectionIndex(proxy: ScrollViewProxy, sections: [String]) -> some View {
self.modifier(SectionIndex(proxy: proxy, sections: sections, titleContent: { title, isSelected in
Text(title.prefix(1))
.font(.system(size: isSelected ? 32 : 16))
.fontWeight(isSelected ? .bold : .regular)
.foregroundColor(.blue)
.padding(.trailing, 3)
}))
}
}
Finally, here's a sample view to test that shows it in action:
struct SectionIndexTest: View {
let contacts = [
"A": ["Anne", "Alfred", "Allie"],
"B": ["Beth", "Bert", "Bart"],
"C": ["Chad"],
"D": ["Don", "Dona", "Dierdre"],
"E": ["Euphrasius", "Elmo"],
"G": ["Gordan", "Gordon", "Goran"],
"H": ["Herb", "Herbert", "Hertie"],
"I": ["Ion"],
"K": ["Kurt", "Kurtrus", "Kent"],
"O": ["Oprah", "Oswald"],
"P": ["Peter", "Percy", "Princess"],
"R": ["Rand", "Ruth", "Rudy"],
"S": ["Steve", "Stephen", "Stephanie"],
"T": ["Tyrone", "Trevor", "Tundra"],
"W": ["Wilfred", "Wynonna"],
]
var body: some View {
let sections = Array(contacts.keys.sorted())
ScrollViewReader { proxy in
List {
ForEach(sections, id: \.self) { letter in
Section(header: Text(letter)) {
ForEach(contacts[letter]!, id: \.self) { contact in
Text(contact)
}
}
.id(letter) // necessary for ScrollViewProxy to work
}
}
.firstLetterSectionIndex(proxy: proxy, sections: sections) // as simple as this
}
}
}