Table views are a fundamental component of almost any iOS app. But most developers don’t use them when they should or get their architecture wrong.

Table views are more versatile than you might think.

For example, many developers make their life harder using a scroll view when a UITableView would be a better choice.

Finally, architecture is crucial for table views. The code of the table view data source often ends inside view controllers when it should go into a separate class.

Even though Apple introduced SwiftUI at WWDC 2019, you won’t be able to use it in your apps until a large portion of users gets on iOS 13.

Until then, you need to know the right approach to using table views with data sources and delegates.

Architecting SwiftUI apps with MVC and MVVM GET THE FREE BOOK NOW

Contents



Section 1 Using dynamic table views

Section 2 Designing reusable cells

Section 3 Table view architecture

Section 4 Selecting cells, adding sections and using delegates

Section 1: Using dynamic table views The first step in using table views is understanding what you can do with them. iOS offers different tools for different tasks. As a developer you need to know which one works better to solve your problem.

When to use scroll views, table views or collection views

At WWDC 2019 Apple introduced SwiftUI, which completely changes the way we build UIs in iOS and other Apple platforms.

But SwiftUI is going to available only from iOS 13 onward. Until all your users get the latest iOS update, you are stuck using UIKit in your apps. That could take a couple of years. Until then, you need to learn how to use table views if you want to become an iOS developer.

The most common mistake I see is not using a UITableView at all and using a scroll view instead.

The UIKit framework offers many classes, all with a specific purpose. If you are serious about iOS development, you should get familiar with Apple’s iOS Human Interface Guidelines.

These guidelines are helpful to pick the right tool. Just looking at the pictures in the guide, it’s clear what each component is meant for.

Let’s start with scroll views:

Scroll views make content scrollable, horizontally, vertically or both. Their content is unstructured, which means there are no repeating elements or patterns.

The tables section of Apple’s guidelines shows the stark difference with scroll views:

Table views manage vertical lists of (repeating) elements. The word vertical is essential here because a UITableView arranges its components into a single column.

There is no such thing as a horizontal table view. To arrange items horizontally, even if scrolling will only be vertical, you need a collection view:

To be precise, table views and collection views are also scroll views. If you look at the documentation of the UITableView class, you will see that it descends from UIScrollView.

Advantages of table views over scroll views

It might look like table views are made only to manage repeating elements. In reality, though they are the right choice for almost any screen that needs to scroll vertically.

The images in Apple’s HIG don’t do table views much justice. Have a look, instead, at this small collection of table view designs I put together on Dribble:

So, let’s see why you should, in most cases, use a UITableView instead of a plain scroll view.

A table view calculates the size and position of each item . In a scroll view, you need to figure the frame of each subview by yourself. You can use Auto Layout, but that still means you will need a lot of code to set all the constraints. In a scroll view, that’s not as easy to get right as it is in a regular UIView .

. In a scroll view, you need to figure the frame of each subview by yourself. You can use Auto Layout, but that still means you will need a lot of code to set all the constraints. In a scroll view, that’s not as easy to get right as it is in a regular . Table views keep the memory footprint of your app small . The views you add to a scroll view stay in memory until you remove them. When an app takes too much memory, it gets terminated by the operating system. A UITableView instead reuses its subviews, needing fewer.

. The views you add to a scroll view stay in memory until you remove them. When an app takes too much memory, it gets terminated by the operating system. A instead reuses its subviews, needing fewer. In a table view, scrolling is smoother (if you get your UITableView code right) . When the user scrolls through the content, the OS continuously calculates the coordinates of a view, even if it’s outside of the screen. This takes resources and consumes the battery faster. Since a table view reuses its subviews, there are fewer views that need to be moved around.

. When the user scrolls through the content, the OS continuously calculates the coordinates of a view, even if it’s outside of the screen. This takes resources and consumes the battery faster. Since a table view reuses its subviews, there are fewer views that need to be moved around. In a UITableView, you can quickly reload, insert, delete, and reorder rows. All these actions come with standard animations out of the box. In a scroll view, you have to write a ton of code to get the same functionality.

All these actions come with standard animations out of the box. In a scroll view, you have to write a ton of code to get the same functionality. A table view can arrange items into sections and use indexes. Moreover, the section headers remain on the screen until the whole section passes. And indexes allow the user to quickly jump around a table view with a lot of content.

So, my recommendation is: if you need scrolling, use a table view (or, eventually, a collection view).

You need scroll views only in a few, specific cases. For example, to display content with a vast area that you can zoom or scroll in any direction, e.g., photos or maps.

For dynamic table views use simple view controllers with a custom UITableView instance

In this article, we will create a small app that displays a list of quotes.

The app will have two screens to explore both plain table views and table views with sections. You can get the complete project on GitHub.

For starters, we need to set up the navigation flow of the app in the Xcode storyboard.

The two navigation controllers provide a navigation bar to each tab. We will also need them later for drill-down navigation.

For our two screens, I picked two plain view controllers. UIKit also provides the UITableViewController class, which comes already equipped with a UITableView instance as its main view.

You need to use a table view controllers if you want a static table view, where you can set the full content directly in the storyboard. Here you can find how to configure static table views.

That works because the UITableViewController already implements some typical behavior. But that gets in our way when we want a dynamic table view, to which we provide content from our code.

There are a couple of reasons to use a plain view controller for dynamic table views:

The main view of a table view controller is a UITableView . This creates problems when you want to add extra elements to your UI since you can’t add subviews to a UITableView instance.

. This creates problems when you want to add extra elements to your UI since you can’t add subviews to a instance. A table view controller acts as the data source for its table view. As we will see later, it’s better to keep the two roles separate.

Most a UITableViewController implementation is for static table views anyway. In dynamic table views we provide content programmatically, so we don’t lose much if we don’t use one.

Section 2: Designing reusable cells Reusable cells are a fundamental piece of a working table view. Not only you need to understand how cells are reused by a table view, but you also need to design properly so that the table view will resize them automatically using Auto Layout.

How a table view reuses cells for scrolling performance and low memory footprint

A table view displays its elements using specialized subviews called cells.

The cells of UITableView are instances of UITableViewCell or its subclasses. It is the table view that adds, removes, and arranges cells in its view hierarchy.

Table views reuse cells that go out of the screen to display new elements, so that:

Performance improves. Instantiating, adding, and removing views from the view hierarchy causes a lot of overhead. It’s far more efficient to reconfigure and move an existing subview to a new location.

Instantiating, adding, and removing views from the view hierarchy causes a lot of overhead. It’s far more efficient to reconfigure and move an existing subview to a new location. The memory footprint remains low. Views are complex objects that take space. Reusing cells keep their number low and saves memory.

All the cells in a table view come from a prototype. A UITableView replicates a prototype as many times as needed to populate its rows.

A table view can have more than one prototype. The number of prototypes depends on how many kinds of elements a table view displays.

For example, a social networking app might show text posts, links, pictures, and videos. A table view would have a prototype for each one of these, so four in total.

Setting up a dynamic table view with custom prototype cells

Since we are using a simple view controller, we have to add a UITableView instance in each of our storyboard scenes.

The first important thing to notice is that the Auto Layout constraints of a table view are pinned to the edges of the view controller scene.

While we usually keep the subviews of a view controller within the safe area, table views are supposed to extend below the navigation and the tab bars. That’s because bars in iOS are translucent, so you can see the content moving below them.

The system also configures the insets of any UITableView to keep cells within the safe area.

We now need a cell prototype.

UIKit offers some standard cells, but are only useful if you want your app to look like one of the stock iOS apps.

Most of the time though you will have cells with a custom design. So you need to create a prototype in the storyboard.

You can set the number of prototypes a table view has through the Identity inspector. Interface builder automatically adds enough empty prototypes to the table view.

Since all the rows in our tables look the same, we need just one prototype for each table view.

Laying out autoresizing table view cells with Auto Layout

A cell prototype in a storyboard is nothing else than a subview, so we arrange its content using Auto Layout constraints.

Here I used a stack view, but you can use simple constraints instead if you prefer.

As in any other view, we pin our subviews to the top and both sides of the cell. I usually use layout margins to simplify my designs, but you can attach your constraints to the edges of the cell if you prefer.

Sometimes, the rows of a table view have all the same height. If that’s the case, you don’t need to do anything else. Your cell instances will have the same height as the prototype.

But in our app, the text of a quote could span many lines, so a cell needs to adapt to its content.

First of all, the label needs to expand to fit the length of its text. We do so by setting its number of rows to 0 in the Inspector panel.

Then, as the label grows, it needs to “push” the edges of the cell to make it grow too. For this, we need to attach an additional constraint to the bottom of our cell prototype.

In the past, we had also to add a couple of lines of code to make a UITableView instance resize its cells, but that’s not necessary anymore. Table views do that by default.

If you need to change these properties, you can find them in the Size inspector of Interface Builder.

Section 3: Table view architecture A working table view requires many moving pieces. It’s not only important to understand how they work, though. You need to organize your code in the correct way to have a solid and scalable architecture in your app.

Table views need a data source to function (and an optional delegate)

Once a table view is configured, many think that all you have left is to pass some data to it.

Unfortunately, that’s not how a UITableView works, and this confuses many of the developers that approach iOS for the first time.

To further optimize memory use and performance, a table view does not get all its data at once.

In this way, you don’t have to load into memory the full data, which might not even fit or be expensive to retrieve.

To function, a table view polls two external objects: a data source and, optionally, a delegate.

These two objects must conform to the UITableViewDataSource and UITableViewDelegate protocols, respectively, but can be instances of any class.

There are two protocols instead of just one because they encapsulate two distinct responsibilities:

The data source provides data to the table view . This includes things like the number of sections, the number of rows in each section, the titles of section headers and footers, and, of course, the table view rows.

. This includes things like the number of sections, the number of rows in each section, the titles of section headers and footers, and, of course, the table view rows. The delegate manages the layout and data manipulation. Here we find things like the height or the indentation of cells, selections, insertion, deletion, and reordering of rows.

The MVC pattern applied to table view architecture

This is the point where most developers get their architecture wrong. It is very tempting to put the data source code inside the view controller that managed the table view.

But that violates the MVC pattern (and a bunch of other software principles), overloading the view controller with responsibilities. That’s why you often hear people talking about massive view controllers in iOS development.

Instead, we have to separate the moving parts that make a table view work according to the four layers of MVC.

We represent the data of a table view through one or more model types .

of a table view through one or more . The table view data source needs to be a model controller, or the view controller’s code will snowball.

needs to be a or the view controller’s code will snowball. The discussion about a table view delegate is a bit more complicated. Since its code is often related to navigation or the UI, it makes sometimes sense to put the delegate code in the view controller . But keep an eye on this code, because it can also grow in size and responsibilities. When that happens, you can put some of it in an interface controller , a layer I added to my Lotus MVC pattern.

is a bit more complicated. Since its code is often related to navigation or the UI, it makes sometimes sense to put the delegate code in the . But keep an eye on this code, because it can also grow in size and responsibilities. When that happens, you can put some of it in an , a layer I added to my Lotus MVC pattern. Finally, cells usually need extra code. This usually ends, again, inside view controllers, but belongs to the view layer and goes inside a custom UITableViewCell subclass.

Let’s start from the model layer, which is straightforward.

1 2 3 4 struct Quote { let author : String let text : String }

We also need some actual data. That would usually come from a separate model controller, for example, one that uses property lists.

But here, for simplicity, I hardcoded some data in a static property of the Quote structure.

1 2 3 4 5 6 7 8 9 extension Quote { static let quotes : [ Quote ] = [ . init ( author : "Jordan B. Peterson" , text : "The purpose of life is finding the largest burden that you can bear and bearing it." ) , . init ( author : "Fyodor Dostoevsky" , text : "To go wrong in one's own way is better than to go right in someone else's." ) , . init ( author : "Albert Einstein" , text : "Two things are infinite: the universe and human stupidity; and I am not sure about the universe." ) , . init ( author : "Fyodor Dostoevsky" , text : "To go wrong in one's own way is better than to go right in someone else's." ) , . init ( author : "Lao Tzu" , text : "The journey of a thousand miles begins with a single step." ) ] }

I put the quotes in random order because that’s how we get data. I will show you later how to sort it.

For space reasons, I only reported some of the data above. You can find the complete data in the full Xcode project.

Creating custom UITableViewCell subclasses to format the data of cells

We will now jump to the view layer, at the other end of the MVC pattern.

There is the wrong practice, that somehow still survives, of using the viewWithTag(_:) method to configure the subviews of a cell.

If that’s what you are using, stop.

This practice comes from an old Apple guide. Luckily, they don’t mention it anymore in the newer version of their documentation, so it will hopefully go away someday.

The correct way is to provide outlets for any subview of a cell’s prototype.

1 2 3 4 class QuoteCell : UITableViewCell { @ IBOutlet weak var quoteLabel : UILabel ! @ IBOutlet weak var authorLabel : UILabel ! }

Don’t forget to set QuoteCell as the type of the cell prototype in the storyboard.

Outlets, though, are not enough.

Subviews, and hence outlets, are part of the internal implementation of a view. If we access them from code outside of QuoteCell, we create tight coupling in our code.

If later, we change how a cell draws its content, we will break all code that accesses these outlets.

The correct way is to make outlets private and provide additional properties with basic types instead.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class QuoteCell : UITableViewCell { @ IBOutlet private weak var quoteLabel : UILabel ! @ IBOutlet private weak var authorLabel : UILabel ! var author : String ? { didSet { authorLabel . text = author ? . authorFormatted ? ? "" } } var quoteText : String ? { didSet { quoteLabel . text = quoteText ? . quoteFormatted ? ? "" } } } extension String { var authorFormatted : String { return "― " + self } var quoteFormatted : String { return "“" + self + "”" } }

As you can see, I also added some extra code to format the data according to the design. That’s also code that usually ends in view controllers but belongs to the view layer.

I used a String extension because later, we will need to utilize this functionality somewhere else.

A better, but more advanced, solution is to use a view model from my revision of the MVVM pattern. I cover how to this for table view cells in my Ultimate Course to Making Professional iOS Apps.

One more thing.

Cell prototypes need to have an identifier to be reused. You can use any string you want, but I tend to use the name of the cell class, which simplifies my data source code.

Putting the data source code in a separate model controller

The only piece left is the data source.

At WWDC 2019, Apple introduced diffable data sources, but those are available only in iOS 13. Again, until most of your users get the latest iOS version, which could take a couple of years, you need to create your own data sources.

As I explained above, we need a separate model controller for it.

1 2 3 4 5 6 7 class QuotesDataSource : NSObject { let quotes : [ Quote ] init ( quotes : [ Quote ] ) { self . quotes = quotes . sorted ( by : < ) } }

Our QuotesDataSource class descends from NSObject because UITableViewDataSource, which we will implement in a moment, is an Objective-C protocol.

Notice that it is here that we sort the quotes for the table view. The quotes order specific to this table view and might be different somewhere else.

We need to define what the < operator means between quotes, or we can’t compile our code. We do that by making our Quote type conform to Comparable and providing an implementation for the < operator.

1 2 3 4 5 6 7 8 9 10 11 struct Quote { let author : String let text : String } extension Quote : Comparable { static func < ( lhs : Quote , rhs : Quote ) -> Bool { if lhs . author < rhs . author { return true } else { return lhs . author == rhs . author && lhs . text < rhs . text } } }

The code above arranges quotes first by author, and then alphabetically.

By the way, even though we only implemented the < operator, that is enough for the Quote type to conform to Comparable, and we get all other comparison operators for free.

How a data source provides data to a table view

While a table view offers many sophisticated features, The UITableViewDataSource protocol has only two required methods. Implementing these two is enough to have a working table view. A UITableView instance calls these two methods to request the information it needs, piece by piece.

The methods of a data source can be called more than once. A UITableView instance does not store any data. When the user scrolls back to a row, the table view will request its data again.

The tableView ( _ : numberOfRowsInSection : ) method tells the table view how many elements a section contains.

The table view uses this information to prepare its scrolling area and display a scroll bar of the appropriate size.

The first screen of our app does not have any section, so this method needs to return the total number of items. We’ll talk more about sections later.

1 2 3 4 5 extension QuotesDataSource : UITableViewDataSource { func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { return quotes . count } }

The tableView ( _ : cellForRowAt : ) method needs to return an already configured cell for a specific row.

A table view identifies each row using an index path, which is a pair of integers representing the section and the row.

Again, since, for now, we don’t have sections, all we need is the row, which we use as an index for the quotes array.

1 2 3 4 5 6 7 8 9 10 11 12 13 extension QuotesDataSource : UITableViewDataSource { func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { return quotes . count } func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : QuoteCell . self ) , for : indexPath ) as ! QuoteCell let quote = quotes [ indexPath . row ] cell . author = quote . author cell . quoteText = quote . text return cell } }

The critical line of code in this method is the call to the dequeueReusableCell(withIdentifier:) methods of UITableView. This returns a cell we can reuse, or creates a new one when none is available.

The QuoteCell class already handles data formatting, so all we need to do here is pass the quote data to the cell instance.

Notice also that I used the class name as the identifier for the cell. That allows me to avoid string literals in my code. Beware though that this only works because we set the class name as the identifier for the prototype in the storyboard.

Section 4: Selecting cells, adding sections and using delegates Table views do more than just displaying a long, scrollable list of items. They are often used in combination with navigation controller for drill-down navigation. You can also group a table view’s content into sections, and provide an index to let the user jump around quickly.

Managing cell selection and navigation with storyboard segues

We did most of the work in the Quote, QuotesDataSource, and QuoteCell types. All we need now is a little glue code in the view controller.

1 2 3 4 5 6 7 8 9 10 class QuotesViewController : UIViewController { @ IBOutlet weak var tableView : UITableView ! let dataSource : QuotesDataSource = . init ( quotes : Quote . quotes ) override func viewDidLoad ( ) { super . viewDidLoad ( ) tableView . dataSource = dataSource tableView . reloadData ( ) } }

It’s rare for a table view to only display content with no interaction. Usually, tapping on a row brings the user to a new screen with more details about the selected item.

Even though we don’t have any more details to show for our quotes, I will add a new view controller as an example.

As usual, this view controller needs a custom class with outlets to populate its UI with the quote it will receive from the QuotesViewController.

1 2 3 4 5 6 7 8 9 10 11 12 class DetailViewController : UIViewController { @ IBOutlet weak var textLabel : UILabel ! @ IBOutlet weak var authorLabel : UILabel ! var quote : Quote ? override func viewDidLoad ( ) { super . viewDidLoad ( ) textLabel . text = quote ? . text . quoteFormatted authorLabel . text = quote ? . author . authorFormatted } }

To trigger navigation when the user taps on a cell, we connect a segue from the cell prototype to the storyboard scene we just added.

Select Show from the pop-up menu that appears when you release dragging. This causes the drill-down navigation in the navigation controller that contains the QuotesViewController.

We now need to pass data from the origin view controller to the destination when the user selects a row in the table view. We do that in the prepare(for:sender:) method of the QuotesViewController.

To get the correct quote, we first get the index path of the selected row from the indexPathForSelectedRow property of UITableView, and then get the corresponding quote from the data source.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class QuotesViewController : UIViewController { @ IBOutlet weak var tableView : UITableView ! let dataSource : QuotesDataSource = . init ( quotes : Quote . quotes ) override func viewDidLoad ( ) { super . viewDidLoad ( ) tableView . dataSource = dataSource tableView . reloadData ( ) } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { if let row = tableView . indexPathForSelectedRow ? . row { let selectedQuote = dataSource . quotes [ row ] ( segue . destination as ? DetailViewController ) ? . quote = selectedQuote } } }

Selecting a cell highlights it. You can disable highlighting in the storyboard, but it’s more common to deselect a cell when the user comes back to this screen.

We do that in the viewWillAppear(_:) method of the QuotesViewController so that the cell animation happens together with the navigation transition.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class QuotesViewController : UIViewController { @ IBOutlet weak var tableView : UITableView ! let dataSource : QuotesDataSource = . init ( quotes : Quote . quotes ) override func viewDidLoad ( ) { super . viewDidLoad ( ) tableView . dataSource = dataSource tableView . reloadData ( ) } override func viewWillAppear ( _ animated : Bool ) { super . viewWillAppear ( animated ) if let indexPath = tableView . indexPathForSelectedRow { tableView . deselectRow ( at : indexPath , animated : true ) } } override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) { if let row = tableView . indexPathForSelectedRow ? . row { let selectedQuote = dataSource . quotes [ row ] ( segue . destination as ? DetailViewController ) ? . quote = selectedQuote } } }

We now have a complete table view with selection and navigation

Arranging the data of a table view in separate sections

Our first table view shows all the quotes as a single list. But a UITableView instance can also group its content into sections.

Let’s create a second view controller, in which we will arrange quotes in sections by their author. We already have a scene in the storyboard, to which we need to add a table view with a cell prototype.

Even though the cells in this table view look different, we can reuse the QuoteCell class for the prototype.

As you can see, besides the cell prototype, there is no difference between our two table views in the storyboard.

That’s because sections are part of the data of the table view. So, it is through the data source that we tell the table view to arrange its sections.

As I mentioned above, a table view refers to sections and rows using index paths.

A data source for a table view with sections requires two new methods:

The numberOfSections ( in : ) method of UITableViewDataSource tells the table view how many sections there are.

method of tells the table view how many sections there are. The tableView ( _ : titleForHeaderInSection : ) tells the table view the title of each section.

Organizing data in the data source of a table view with sections

The most important aspect when working with sections is how to arrange data. Once you do that, the code of a data source follows easily.

Here we have two options: a dictionary of arrays, or an array of arrays. The external data structure represents the sections, and the nested arrays their rows. Which solution you pick depends on the implementation details of your app.

In our sample app, we can choose, so I will use a dictionary, which works best in this case.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class AuthorsDataSource : NSObject { private var sections : [ String : [ Quote ] ] = [ : ] init ( quotes : [ Quote ] ) { for quote in quotes . sorted ( by : < ) { let author = quote . author if var quotes = sections [ author ] { quotes . append ( quote ) sections [ author ] = quotes } else { sections [ author ] = [ quote ] } } } }

Again, we arrange the data in the data source and not globally.

How each data source arranges rows is unrelated from how the app organizes data globally, which might depend on other factors — for example, the structure of the data that comes from a network request to a REST API.

Once our data is arranged correctly, the data source methods only need to retrieve the correct information.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class AuthorsDataSource : NSObject { private var sections : [ String : [ Quote ] ] = [ : ] var authors : [ String ] { return sections . keys . sorted ( ) } init ( quotes : [ Quote ] ) { for quote in quotes . sorted ( by : < ) { let author = quote . author if var quotes = sections [ author ] { quotes . append ( quote ) sections [ author ] = quotes } else { sections [ author ] = [ quote ] } } } } extension AuthorsDataSource : UITableViewDataSource { func numberOfSections ( in tableView : UITableView ) -> Int { return sections . count } func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { let author = authors [ section ] return sections [ author ] ? . count ? ? 0 } func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : QuoteCell . self ) ) as ! QuoteCell let author = authors [ indexPath . section ] let quote = sections [ author ] ? [ indexPath . row ] cell . quoteText = quote ? . text return cell } func tableView ( _ tableView : UITableView , titleForHeaderInSection section : Int ) -> String ? { return authors [ section ] } }

Our table view is now divided into sections, with headers containing the name of each author.

Using the table view delegate to create custom section headers

Our sections work, but the headers do not look like the ones in our mockup.

Unfortunately, those headers are managed by the table view, and we cannot customize them. If we want them to look different, we have to create our own.

Luckily, it’s not hard.

Since the section headers of a UITableView are all identical, we can create a prototype in a nib file from which we will create all instances.

Views in a nib file usually get the size and shape of a view controller, but if you set the Size to Freeform, you can resize a view as you like. Don’t worry too much about getting it right, though. The table view will resize each instance.

To keep code well-separated according to the MVC pattern, it’s also a good practice to create a custom class for our headers.

1 2 3 4 5 6 7 class AuthorHeaderView : UIView { @ IBOutlet private weak var label : UILabel ! var author : String ? { didSet { label . text = author } } }

And finally, we create each instance in the table view delegate, in the tableView(_:viewForHeaderInSection:) method.

1 2 3 4 5 6 7 8 9 10 11 extension AuthorsViewController : UITableViewDelegate { func tableView ( _ tableView : UITableView , viewForHeaderInSection section : Int ) -> UIView ? { let view = UINib ( nibName : "SectionHeader" , bundle : nil ) . instantiate ( withOwner : nil , options : nil ) . first as ? AuthorHeaderView view ? . author = dataSource . authors [ section ] return view } func tableView ( _ tableView : UITableView , heightForHeaderInSection section : Int ) -> CGFloat { return 33 . 0 } }

Don’t forget to set the table view delegate in the storyboard. After that, the table view will use our custom headers.

The table view adapts the width of each header view to fill the screen horizontally. Their height is instead determined by the tableView(_:heightForHeaderInSection:) method.

Notice that this time it’s the view controller that conforms to UITableViewDelegate. I did not create a separate class as I did for the data sources.

This is because a table view delegate has a more varied role than a data source, and it often needs to interact with other objects.

For example, in the code above, we retrieve the section titles from the data source. Having a separate delegate object would make our architecture more complex.

Adding an index to a table view to jump between sections

One final, convenient feature that table views offer is an index that the user can use to jump directly to a specific section. And we can implement it with just a few lines of code.

All we have to do is return an array of strings from the sectionIndexTitles(for:) of UITableViewDataSource, containing the indexes we want.

In our case, we can use the initial of an author’s name as the index in the table view.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 extension AuthorsDataSource : UITableViewDataSource { func numberOfSections ( in tableView : UITableView ) -> Int { return sections . count } func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { let author = authors [ section ] return sections [ author ] ? . count ? ? 0 } func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : QuoteCell . self ) ) as ! QuoteCell let author = authors [ indexPath . section ] let quote = sections [ author ] ? [ indexPath . row ] cell . quoteText = quote ? . text return cell } func sectionIndexTitles ( for tableView : UITableView ) -> [ String ] ? { return authors . map { String ( $ 0 . first ! ) } } }

Some authors have names starting with the same letter, so we get duplicate indexes in the table view.

There are many ways to remove duplicates from an array. One way is to use a Swift Set.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 class AuthorsDataSource : NSObject { private var sections : [ String : [ Quote ] ] = [ : ] var authors : [ String ] { return sections . keys . sorted ( ) } var indexes : [ String ] { return authors . map { String ( $ 0 . first ! ) } . reduce ( into : Set < String > ( ) , { $ 0 . insert ( $ 1 ) } ) . sorted ( ) } init ( quotes : [ Quote ] ) { for quote in quotes . sorted ( by : < ) { let author = quote . author if var quotes = sections [ author ] { quotes . append ( quote ) sections [ author ] = quotes } else { sections [ author ] = [ quote ] } } } } extension AuthorsDataSource : UITableViewDataSource { func numberOfSections ( in tableView : UITableView ) -> Int { return sections . count } func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { let author = authors [ section ] return sections [ author ] ? . count ? ? 0 } func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : QuoteCell . self ) ) as ! QuoteCell let author = authors [ indexPath . section ] let quote = sections [ author ] ? [ indexPath . row ] cell . quoteText = quote ? . text return cell } func sectionIndexTitles ( for tableView : UITableView ) -> [ String ] ? { return indexes } }

I tend to write this type of code following the functional programming approach of chaining map(_:) and reduce(_:).

If you prefer, you can use a for loop on the authors array and add each letter to a Set variable.

Unfortunately, removing duplicates from the index creates another problem. Now the indexes do not correspond anymore to the sections in the table view. Tapping on a letter brings makes the table view jump to the wrong section.

Luckily, that’s easy to fix.

The UITableViewDataSource protocol includes the tableView(_:sectionForSectionIndexTitle:at:) method to balance indexes with sections. All we have to do is find the first author in the authors array with a specific initial.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 extension AuthorsDataSource : UITableViewDataSource { func numberOfSections ( in tableView : UITableView ) -> Int { return sections . count } func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { let author = authors [ section ] return sections [ author ] ? . count ? ? 0 } func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { let cell = tableView . dequeueReusableCell ( withIdentifier : String ( describing : QuoteCell . self ) ) as ! QuoteCell let author = authors [ indexPath . section ] let quote = sections [ author ] ? [ indexPath . row ] cell . quoteText = quote ? . text return cell } func sectionIndexTitles ( for tableView : UITableView ) -> [ String ] ? { return indexes } func tableView ( _ tableView : UITableView , sectionForSectionIndexTitle title : String , at index : Int ) -> Int { return authors . firstIndex ( where : { $ 0 . hasPrefix ( title ) } ) ? ? 0 } }

Summary

We have seen how table views work and how to configure them. This was a simple example, but it already involves many types and concepts.

With table views, it is essential to understand:

What we need them for : table views can display not only lists of repeating elements but also any other kind of vertically scrollable content.

: table views can display not only lists of repeating elements but also any other kind of vertically scrollable content. How they work : table views display elements through reusable cells, to enable smooth scrolling and to keep the memory footprint of your apps low.

: table views display elements through reusable cells, to enable smooth scrolling and to keep the memory footprint of your apps low. How to configure them : table views don’t get their data as a whole. Instead they a data source for it only when needed.

: table views don’t get their data as a whole. Instead they a data source for it only when needed. How to keep code separate : a lot of code related to table views needs to be in different classes: the model layer represents data; code related to visual appearance should go in custom cells; a separate class should adopt the UITableViewDataSource protocol and not by the view controller; the only role of the view controller is to act as a bridge.

: a lot of code related to table views needs to be in different classes:

It is likely that, in a real-world app, you want more out of your table views. These are a few common paths you might want to explore:

Loading data from the disk;

Centralizing the creation of shared model controllers;

Adding new quotes;

Updating the table view when the data changes;