If you'd like to show a large number of records in UITableView, you'd best rethink the approach of how to display your data. As the number of rows grows, the table view becomes unwieldy. One way to improve the user experience is to organize the data into sections. By grouping related data together, you offer a better way for users to access it.
Furthermore, you can implement an index list in the table view. An indexed table view is more or less the same as the plain-styled table view. The only difference is that it includes an index on the right side of the table view. An indexed table is very common in iOS apps. The most well-known example is the built-in Contacts app on the iPhone. By offering index scrolling, users have the ability to access a particular section of the table instantly without scrolling through each section.
Let's see how we can add sections and an index list to a simple table app.
If you have a basic understanding of the UITableView
implementation using diffable data source, it's not too difficult to add sections. And to add an index list, you need to override these methods as defined in the UITableViewDiffableDataSource
class:
tableView(_:titleForHeaderInSection:)
method – returns the header titles for different sections. This method is optional if you do not prefer to assign titles to the section.sectionIndexTitles(for:)
method – returns the indexed titles that appear in the index list on the right side of the table view. For example, you can return an array of strings containing a value from A
to Z
.tableView(_:sectionForSectionIndexTitle:at:)
method – returns the section index that the table view should jump to when a user taps a particular index.There is no better way to explain the implementation than showing you an example. As usual, we will build a simple app, which should give you a better idea of an index list implementation using the diffable data source approach.
First, let's have a quick look at the demo app that we are going to build. It's a very simple app showing a list of animals in a standard table view. Instead of listing all the animals, the app groups the animals into different sections, and displays an index list for quick access. The screenshot below displays the final deliverable of the demo app.
The focus of this demo is on the implementation of sections and index list. Therefore, instead of building the Xcode project from scratch, you can download the project template from http://www.appcoda.com/resources/swift55/IndexedTableDemoStarter.zip to start with.
The template already includes everything you need to start with. I already implemented the table view using diffable data source, which has been covered in the beginner book. In the AnimalTableViewController.swift
file, you should find an extension which provides two methods for configuring the diffable data source and the data snapshot.
extension AnimalTableViewController {
func configureDataSource() -> UITableViewDiffableDataSource<String, String> {
let dataSource = UITableViewDiffableDataSource<String, String>(tableView: tableView) { (tableView, indexPath, animalName) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
// Configure the cell...
cell.textLabel?.text = animalName
// Convert the animal name to lower case and
// then replace all occurences of a space with an underscore
let imageFileName = animalName.lowercased().replacingOccurrences(of: " ", with: "_")
cell.imageView?.image = UIImage(named: imageFileName)
return cell
}
return dataSource
}
func updateSnapshot(animatingChange: Bool = false) {
// Create a snapshot and populate the data
var snapshot = NSDiffableDataSourceSnapshot<String, String>()
snapshot.appendSections(["all"])
snapshot.appendItems(animals, toSection: "all")
dataSource.apply(snapshot, animatingDifferences: false)
}
}
If you build the template, you'll have an app showing a list of animals in a table view (but without sections and index). Later, we will modify the app, group the data into sections, and add an index list to the table.
Okay, let's get started. If you open the IndexTableDemo
project, the animal data is defined in an array:
let animals = ["Bear", "Black Swan", "Buffalo", "Camel", "Cockatoo", "Dog", "Donkey", "Emu", "Giraffe", "Greater Rhea", "Hippopotamus", "Horse", "Koala", "Lion", "Llama", "Manatus", "Meerkat", "Panda", "Peacock", "Pig", "Platypus", "Polar Bear", "Rhinoceros", "Seagull", "Tasmania Devil", "Whale", "Whale Shark", "Wombat"]
Well, we're going to organize the data into sections based on the first letter of the animal name. There are a lot of ways to do that. One way is to manually replace the animals array with a dictionary like I've shown below:
let animals: [String: [String]] = ["B" : ["Bear", "Black Swan", "Buffalo"],
"C" : ["Camel", "Cockatoo"],
"D" : ["Dog", "Donkey"],
"E" : ["Emu"],
"G" : ["Giraffe", "Greater Rhea"],
"H" : ["Hippopotamus", "Horse"],
"K" : ["Koala"],
"L" : ["Lion", "Llama"],
"M" : ["Manatus", "Meerkat"],
"P" : ["Panda", "Peacock", "Pig", "Platypus", "Polar Bear"],
"R" : ["Rhinoceros"],
"S" : ["Seagull"],
"T" : ["Tasmania Devil"],
"W" : ["Whale", "Whale Shark", "Wombat"]]
In the code above, we've turned the animals array into a dictionary. The first letter of the animal name is used as a key. The value that is associated with the corresponding key is an array of animal names.
We can manually create the dictionary, but wouldn't it be great if we could create the indexes from the animals
array programmatically? Let's see how it can be done.
First, declare two instance variables in the AnimalTableViewController
class:
var animalsDict = [String: [String]]()
var animalSectionTitles = [String]()
We initialize an empty dictionary for storing the animals and an empty array for storing the section titles of the table. The section title is the first letter of the animal name (e.g. B).
Because we want to generate a dictionary from the animals
array, we need a helper method to handle the generation. Insert the following method in the AnimalTableViewController
class:
private func createAnimalDict() {
for animal in animals {
// Get the first letter of the animal name and build the dictionary
let firstLetterIndex = animal.index(animal.startIndex, offsetBy: 1)
let animalKey = String(animal[..<firstLetterIndex])
if var animalValues = animalsDict[animalKey] {
animalValues.append(animal)
animalsDict[animalKey] = animalValues
} else {
animalsDict[animalKey] = [animal]
}
}
// Get the section titles from the dictionary's keys and sort them in ascending order
animalSectionTitles = [String](animalsDict.keys)
animalSectionTitles = animalSectionTitles.sorted(by: { $0 < $1 })
}
In this method, we loop through all the items in the animals
array. For each item, we initially extract the first letter of the animal's name. To obtain an index for a specific position (i.e. String.Index
), you have to ask the string itself for the startIndex
and then call the index
method to get the desired position. In this case, the target position is 1
, since we are only interested in the first character.
In older version of Swift, you use substring(to:)
method of a string to get a new string containing the characters up to a given index. Now, the method has been deprecated. Instead, you slice a string into a substring using subscripting like this:
let animalKey = String(animal[..<firstLetterIndex])
animal[..<firstLetterIndex]
slices the animal
string up to the specified index. In the above case, it means to extract the first character. You may wonder why we need to wrap the returned substring with a String initialization. In Swift 4, when you slice a string into a substring, you will get a Substring
instance. It is a temporary object, sharing its storage with the original string. In order to convert a Substring
instance to a String
instance, you will need to wrap it with String()
.
As mentioned before, the first letter of the animal's name is used as a key of the dictionary. The value of the dictionary is an array of animals of that particular key. Therefore, once we got the key, we either create a new array of animals or append the item to an existing array. Here we show the values of animalsDict
for the first four iterations:
animalsDict["B"] = ["Bear"]
animalsDict["B"] = ["Bear", "Black Swan"]
animalsDict["B"] = ["Bear", "Black Swan", "Buffalo"]
animalsDict["C"] = ["Camel"]
After animalsDict
is completely generated, we can retrieve the section titles from the keys of the dictionary.
To retrieve the keys of a dictionary, you can simply call the keys
method. However, the keys returned are unordered. Swift's standard library provides a function called sorted
, which returns a sorted array of values of a known type, based on the output of a sorting closure you provide.
The closure takes two arguments of the same type (in this example, it's the string) and returns a Bool
value to state whether the first value should appear before or after the second value once the values are sorted. If the first value should appear before the second value, it should return true.
One way to write the sort closure is like this:
animalSectionTitles = animalSectionTitles.sorted( by: { (s1:String, s2:String) -> Bool in
return s1 < s2
})
You should be very familiar with the closure expression syntax. In the body of the closure, we compare the two string values. It returns true
if the second value is greater than the first value. For instance, the value of s1
is B
and that of s2
is E
. Because B is smaller than E, the closure returns true, indicating that B should appear before E. In this case, we can sort the values in alphabetical order.
If you read the earlier code snippet carefully, you may wonder why I wrote the sort
closure like this:
animalSectionTitles = animalSectionTitles.sorted(by: { $0 < $1 })
It's a shorthand in Swift for writing inline closures. Here $0
and $1
refer to the first and second String arguments. If you use shorthand argument names, you can omit nearly everything of the closure including argument list and in keyword; you will just need to write the body of the closure.
Swift provides another sort function called sort
. This function is very similar to the sorted
function. Instead of returning you a sorted array, the sort
function applies the sorting on the original array. You can replace the line of code with the one below:
animalSectionTitles.sort(by: { $0 < $1 })
With the helper method created, update the viewDidLoad
method to call it up:
override func viewDidLoad() {
super.viewDidLoad()
// Generate the animal dictionary
createAnimalDict()
}
Next, modify the updateSnapshot
method like this to organize the item into different sections:
func updateSnapshot(animatingChange: Bool = false) {
// Create a snapshot and populate the data
var snapshot = NSDiffableDataSourceSnapshot<String, String>()
snapshot.appendSections(animalSectionTitles)
animalSectionTitles.forEach { (section) in
if let animals = animalsDict[section] {
snapshot.appendItems(animals, toSection: section)
}
}
dataSource.apply(snapshot, animatingDifferences: false)
}
Instead of putting all items in the "all" section, we modify the code to create a snapshot with different sections (i.e. snapshot.appendSections(animalSectionTitles)
). And, for each section, we add the correct animal items to it.
If you run the app now, the app should look exactly the same as before, though you already arrange the items into different sections.
What you need to do next is to give each section a title. In order to do that, you need to override the following method of the UITableViewDiffableDataSource
protocol:
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
}
In the starter project, we use the UITableViewDiffableDataSource
class directly. To override the method above and provide our own implementation, we have to create a custom diffable data source. In the project navigator, right click IndexedTableDemo
and choose New file.... Use the Swift file template and name it AnimalTableDataSource
.
Now update the file's content like this:
import UIKit
class AnimalTableDataSource: UITableViewDiffableDataSource<String, String> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.snapshot().sectionIdentifiers[section]
}
}
We create a custom diffable data source by extending UITableViewDiffableDataSource
and override the required method. In the method, we simply return the title of the given section number. Next, switch over to AnimalTableViewController.swift
and update the configureDataSource()
method like this:
func configureDataSource() -> AnimalTableDataSource {
let dataSource = AnimalTableDataSource(tableView: tableView) { (tableView, indexPath, animalName) -> UITableViewCell? in
.
.
.
}
return dataSource
}
Instead of using the default UITableViewDiffableDataSource
, we replace it with our own data source called AnimalTableDataSource
.
Okay, you're ready to go! Hit the Run button and you should end up with an app with sections.
So how can you add an index list to the table view? Again it's easier than you thought and can be achieved with just a few lines of code. Simply add the following methods in the AnimableTableDataSource
:
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return self.snapshot().sectionIdentifiers
}
override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
return index
}
The first method provides the content of the index list. Here we return the section titles. The second method returns the index of the section having the given title and section title index. By implementing this method, your app can scroll to a particular section of the table when the user taps any of the indexes.
That's it! Compile and run the app again. You should find the index on the right side of the table. When you tap one of the indexes, it will jump directly to that section.
Looks like we've done everything. Currently, the index list doesn't contain the entire alphabet. It just shows those letters that are defined as the keys of the animals
dictionary. Sometimes, you may want to display A-Z in the index list. Let's change the sectionIndexTitles(for:)
method in AnimalTableDataSource
and return the full list of alphabet:
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return ["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"]
}
Now, compile and run the app again. Cool! The app displays the index from A to Z.
But wait a minute… It doesn't work properly! If you try tapping the index "C," the app jumps to the "D" section. And if you tap the index "G," it directs you to the "K" section. Below shows the mapping between the old and new indexes.

Well, as you may notice, the number of indexes is greater than the number of sections, and the
UITableView
object doesn't know how to handle the indexing. It's your responsibility to update the tableView(_:sectionForSectionIndexTitle:at:)
method and explicitly tell the table view the section number when a particular index is tapped. Modify the following method in AnimalTableDataSource
like this:
override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
guard let index = self.snapshot().sectionIdentifiers.firstIndex(of: title) else {
return -1
}
return index
}
The whole point of the implementation is to verify if the given title can be found in the section identifiers and return the corresponding index. Then the table view moves to the corresponding section. For instance, if the title is B
, we check that B
is a valid section title and return the index 1
. In case the title is not found (e.g. A
), we return -1
.
Compile and run the app again. The index list should now work!
You can easily customize the section headers by overriding some of the methods defined in the UITableView
class and the UITableViewDelegate
protocol. In this demo, we'll make a couple of simple changes:
To alter the height of the section header, you can simply override the tableView(_:heightForHeaderInSection:)
method and return the preferred height:
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 50
}
Before the section header view is displayed, the tableView(_:willDisplayHeaderView:forSection:)
method will be called. The method includes an argument named view
. This view object can be a custom header view or a standard one. In our demo, we just use the standard header view, which is the UITableViewHeaderFooterView
object. Once you have the header view, you can alter the text color, font, and background color.
override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let headerView = view as! UITableViewHeaderFooterView
headerView.backgroundView?.backgroundColor = UIColor(red: 236.0/255.0, green: 240.0/255.0, blue: 241.0/255.0, alpha: 1.0)
headerView.textLabel?.textColor = UIColor(red: 231.0/255.0, green: 76.0/255.0, blue: 60.0/255.0, alpha: 1.0)
headerView.textLabel?.font = UIFont(name: "Avenir", size: 25.0)
}
Run the app again. The header view should be updated with your preferred font and color.
When you need to display a large number of records, it is simple and effective to organize the data into sections and provide an index list for easy access. In this chapter, we've walked you through the implementation of an indexed table. By now, I believe you should know how to add sections and an index list to your table view.
For reference, you can download the complete Xcode project from http://www.appcoda.com/resources/swift55/IndexedTableDemo.zip.