Is it possible to have a List with an index on the right hand side, like the example below in SwiftUI?
<
See the solution provided on this page by DirectX and please consider giving it an upvote. It is the correct answer.
I've taken his View and created a ViewModifier you can use with any view that consists of a SwiftUI List with Sections (tableview).
Just be sure to provide a list of header (section) titles that corresponds to the headers in the view you are adding the index to. Click on the letter to scroll to that section of the list. Notice that I only provide the indexes I can actually scroll to when calling the view modifier.
Use like any view modifier:
SimpleDemoView().modifier(VerticalIndex(indexableList: contacts))
Here's the code for the modifier:
struct VerticalIndex: ViewModifier {
let indexableList: [String]
func body(content: Content) -> some View {
var body: some View {
ScrollViewReader { scrollProxy in
ZStack {
content
VStack {
ForEach(indexableList, id: \.self) { letter in
HStack {
Spacer()
Button(action: {
withAnimation {
scrollProxy.scrollTo(letter)
}
}, label: {
Text(letter)
.font(.system(size: 12))
.padding(.trailing, 7)
})
}
}
}
}
}
}
return body
}
}
Here's what it looks like using the sample provided by DirectX:
For completeness, here's code to reproduce the display:
struct SimpleDemo_Previews: PreviewProvider {
static var previews: some View {
SimpleDemoView().modifier(VerticalIndex(indexableList: contacts))
}
}
struct SimpleDemoView: View {
var body: some View {
List {
ForEach(alphabet, id: \.self) { letter in
Section(header: Text(letter).id(letter)) {
ForEach(contacts.filter({ (contact) -> Bool in
contact.lastName.prefix(1) == letter
})) { contact in
HStack {
Image(systemName: "person.circle.fill").font(.largeTitle).padding(.trailing, 5)
Text(contact.firstName)
Text(contact.lastName)
}
}
}
}
}
.navigationTitle("Contacts")
.listStyle(PlainListStyle())
}
}
Here's the sample data used to provide the demo (modified from DirectX's solution):
let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"] //swiftlint:disable comma
let contacts: [Contact] = {
var contacts = [Contact]()
contacts.append(Contact(firstName: "Chris", lastName: "Ryan"))
contacts.append(Contact(firstName: "Allyson", lastName: "Ryan"))
contacts.append(Contact(firstName: "Jonathan", lastName: "Ryan"))
contacts.append(Contact(firstName: "Brendan", lastName: "Ryaan"))
contacts.append(Contact(firstName: "Jaxon", lastName: "Riner"))
contacts.append(Contact(firstName: "Leif", lastName: "Adams"))
contacts.append(Contact(firstName: "Frank", lastName: "Conors"))
contacts.append(Contact(firstName: "Allyssa", lastName: "Bishop"))
contacts.append(Contact(firstName: "Justin", lastName: "Bishop"))
contacts.append(Contact(firstName: "Johnny", lastName: "Appleseed"))
contacts.append(Contact(firstName: "George", lastName: "Washingotn"))
contacts.append(Contact(firstName: "Abraham", lastName: "Lincoln"))
contacts.append(Contact(firstName: "Steve", lastName: "Jobs"))
contacts.append(Contact(firstName: "Steve", lastName: "Woz"))
contacts.append(Contact(firstName: "Bill", lastName: "Gates"))
contacts.append(Contact(firstName: "Donald", lastName: "Trump"))
contacts.append(Contact(firstName: "Darth", lastName: "Vader"))
contacts.append(Contact(firstName: "Clark", lastName: "Kent"))
contacts.append(Contact(firstName: "Bruce", lastName: "Wayne"))
contacts.append(Contact(firstName: "John", lastName: "Doe"))
contacts.append(Contact(firstName: "Jane", lastName: "Doe"))
return contacts.sorted()
}()
let indexes = Array(Set(contacts.compactMap({ String($0.lastName.prefix(1)) }))).sorted()
I was looking for a solution to the same question , but it currently the only option that we might have right now is using UITableView
as View.
import SwiftUI
import UIKit
struct TableView: UIViewRepresentable {
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
final class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellId = "cellIdentifier"
let cell = tableView.dequeueReusableCell(withIdentifier: cellId) ?? UITableViewCell(style: .default, reuseIdentifier: cellId)
cell.textLabel?.text = "\(indexPath)"
return cell
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
["a", "b"]
}
}
I did this in SwiftUI
//
// Contacts.swift
// TestCalendar
//
// Created by Christopher Riner on 9/11/20.
//
import SwiftUI
struct Contact: Identifiable, Comparable {
static func < (lhs: Contact, rhs: Contact) -> Bool {
return (lhs.lastName, lhs.firstName) < (rhs.lastName, rhs.firstName)
}
var id = UUID()
let firstName: String
let lastName: String
}
let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
struct Contacts: View {
@State private var searchText = ""
var contacts = [Contact]()
var body: some View {
VStack {
ScrollViewReader { scrollProxy in
ZStack {
List {
SearchBar(searchText: $searchText)
.padding(EdgeInsets(top: 0, leading: -20, bottom: 0, trailing: -20))
ForEach(alphabet, id: \.self) { letter in
Section(header: Text(letter).id(letter)) {
ForEach(contacts.filter({ (contact) -> Bool in
contact.lastName.prefix(1) == letter
})) { contact in
HStack {
Image(systemName: "person.circle.fill").font(.largeTitle).padding(.trailing, 5)
Text(contact.firstName)
Text(contact.lastName)
}
}
}
}
}
.navigationTitle("Contacts")
.listStyle(PlainListStyle())
.resignKeyboardOnDragGesture()
VStack {
ForEach(alphabet, id: \.self) { letter in
HStack {
Spacer()
Button(action: {
print("letter = \(letter)")
//need to figure out if there is a name in this section before I allow scrollto or it will crash
if contacts.first(where: { $0.lastName.prefix(1) == letter }) != nil {
withAnimation {
scrollProxy.scrollTo(letter)
}
}
}, label: {
Text(letter)
.font(.system(size: 12))
.padding(.trailing, 7)
})
}
}
}
}
}
}
}
init() {
contacts.append(Contact(firstName: "Chris", lastName: "Ryan"))
contacts.append(Contact(firstName: "Allyson", lastName: "Ryan"))
contacts.append(Contact(firstName: "Jonathan", lastName: "Ryan"))
contacts.append(Contact(firstName: "Brendan", lastName: "Ryaan"))
contacts.append(Contact(firstName: "Jaxon", lastName: "Riner"))
contacts.append(Contact(firstName: "Leif", lastName: "Adams"))
contacts.append(Contact(firstName: "Frank", lastName: "Conors"))
contacts.append(Contact(firstName: "Allyssa", lastName: "Bishop"))
contacts.append(Contact(firstName: "Justin", lastName: "Bishop"))
contacts.append(Contact(firstName: "Johnny", lastName: "Appleseed"))
contacts.append(Contact(firstName: "George", lastName: "Washingotn"))
contacts.append(Contact(firstName: "Abraham", lastName: "Lincoln"))
contacts.append(Contact(firstName: "Steve", lastName: "Jobs"))
contacts.append(Contact(firstName: "Steve", lastName: "Woz"))
contacts.append(Contact(firstName: "Bill", lastName: "Gates"))
contacts.append(Contact(firstName: "Donald", lastName: "Trump"))
contacts.append(Contact(firstName: "Darth", lastName: "Vader"))
contacts.append(Contact(firstName: "Clark", lastName: "Kent"))
contacts.append(Contact(firstName: "Bruce", lastName: "Wayne"))
contacts.append(Contact(firstName: "John", lastName: "Doe"))
contacts.append(Contact(firstName: "Jane", lastName: "Doe"))
contacts.sort()
}
}
struct Contacts_Previews: PreviewProvider {
static var previews: some View {
Contacts()
}
}
I've made a couple of changes to @Mozahler's and @DirectX's code, refining the result.
I didn't want the main list to include headers with no content, so in the implementation the line under List { becomes:
ForEach(indexes, id: \.self) { letter in
rather than
ForEach(alphabet, id: \.self) { letter in
Setting a background and uniform width for the index column sets it off from any background and unifies the result:
Text(letter)
.frame(width: 16)
.foregroundColor(Constants.color.textColor)
.background(Color.secondary.opacity(0.5))
.font(Constants.font.customFootnoteFont)
.padding(.trailing, 7)