UITableview: The Essential Solution for Scrolling iOS Interfaces

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.

FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices

MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.

DOWNLOAD THE FREE GUIDE

Contents


TOC 1

Section 1

Using dynamic table views


TOC 2

Section 2

Designing reusable cells


TOC 3

Section 3

Table view architecture


TOC 4

Section 4

Selecting cells, adding sections and using delegates

Section 1:

Using dynamic table views

Mapping JSON data to Swift types and automatic decoding

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 in apple’s human interface guidelines

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:

tables in apple’s human interface guidelines

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:

collection views in apple’s human interface guidelines

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:

a small dribble collection of iOS table views

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.
  • 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.
  • 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.
  • 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.
  • 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.

the mockup for the quotes app

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

the navigation flow in the storyboard for the quotes app

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.
  • 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.
  • The memory footprint remains low. Views are complex objects that take space. Reusing cells keep their number low and saves memory.

how a uitableview reuses cells]

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.

setting a uitableview in a plain view controller scene in interface builder

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.

creating cell prototypes for a dynamic uitableview in a storyboard

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.

setting the auto layout constraint for an autoresizing cell prototype in interface builder

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.

setting the number of lines for an expanding label in a cell prototype in interface builder

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.

setting the bottom Auto Layout constraint for an autoresizing cell prototype in interface buider

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.

setting the automatic row height and estimate for a uitableview in interface builder

Section 3:

Table view architecture

Flattening JSON data and decoding arrays with decoding containers

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.
  • 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.
  • The table view data source needs to be a model controller, 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.
  • 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.

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.

>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.

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.

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.

setting the cell identifier for a custom prototype in interface builder

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.

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.

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.

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.

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

Decoding JSON data in a real iOS app

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.

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.

the detail view controller scene in the storyboard for the quotes app]

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

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.

connecting a segue from a uitableview cell prototype to another view controller scene in a storyboard

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.

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.

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.

the storyboard scene for a uitableview with sections

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.

how a uitableview organizes content in sections 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.
  • 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.

>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.

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.

the nib file for a uitableview custom section header

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.

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.

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.

a uitableview with custom section 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.

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.

duplicate indexes for a uitableview with sections in the ios simulator

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

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.

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.
  • How they work: 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.
  • 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.

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;

SwiftUI App Architecture: Design Patterns and Best Practices

It's easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.

GET THE FREE BOOK NOW

61 thoughts on “UITableview: The Essential Solution for Scrolling iOS Interfaces”

  1. it was really good tutorial. I am a new about leaning swift. I have one question, You sad that “creating this data in the view controller is not correct”, in your opinion Where we would create that that data instead of inside of view controller?

    Reply
    • Generally speaking, data retrieval or creation is not the duty of a view controller. Data could come from the network or from some storage solution in the app (like property lists or Core Data).

      In any case, starting with this small example where data is created in code, there should be a separate object that takes care of it, to which the view controller should ask for the data. Where this data comes from should not be a concern of the view controller.

      Reply
  2. Hi Matteo, I’m attempting to set up a picker cell in an app I’m creating. I have set up the model and the model for the picker as you suggest and created two separate classes for the data source and delegate. The picker has 2 components and the second component updates based on the selection of the first component. What I’m having trouble with is getting the final results of the picker selection to update in the text fields in the view. I’m not sure how to get the picker delegate to communicate the picker selection back to the view controller. Please help

    Reply
    • I don’t really understand what you are trying to do from this description.

      If you are trying to update a table view cell after some changes though, you probably should make the table view reload its data and not go and change the cell itself.

      Reply
  3. I am incredibly surprised that your still finding developers are using scroll views instead of tableviews / collection views.

    I wonder how these so called developers learn they skills.

    Surely one would see the memory issues by using a non recycling protocol*

    Reply
    • I worked for some companies where developers from other fields were put to create apps from scratch. It’s no surprise that they miss even the foundations if iOS development when required to build a full app from zero with no experience.

      Also, this is not the only thing I have seen. Freelancing for different clients exposes you to things you would never think are possible. Not all are like that of course, there are also very skilled developers in some companies.

      Reply
      • Yeah, I have found the same thing as a contractor. Some older system is getting decommissioned. So they recycle those existing developers to start a new project with no experience and they are learning as they go.

        Reply
    • None of the two. It’s a model struct, which means it represents the data of the app. As such, in a larger app it would probably be used in many different places, so it should not belong to any specific class.

      Reply
  4. Also, when you do this:

    let quotes = [
    Quote(author: “Albert Einstein”, text: “Two things are infinite: the universe and human stupidity; and I am not sure about the universe.”),
    Quote(author: “Steve Jobs”, text: “Design is not just what it looks like and feels like. Design is how it works.”),
    Quote(author: “John Lennon”, text: “Life is what happens when you’re busy making other plans.”)
    ]

    I am trying to substitute by my API request but then the problem that I receive is that nothing can with ” self.” can be executed before super.init, so I moved after. But then inside my DataSource the quotes.count on method numberOfRowsInSection comes without objects, probably because inside the ViewController, when the following line is executed the object quotes is not yet created and fullfilled:

    self.dataSource = QuotesDataSource(quotes: quotes)

    Any suggestions on how to change this structure so I can API request to bring the content to the cell?

    Reply
    • If you are requesting data from an API, you won’t have it in the init method of the view controller, so you have to declare the dataSource property as an optional variable like this:

      var dataSource: QuotesDataSource?

      In this way it is initialized with nil automatically. When your network request returns data, then you can create a dataSource in the same way:

      self.dataSource = QuotesDataSource(quotes: quotes)

      Reply
  5. If I create a separate data source class and that class loads data asynchronously, how can that class cause the tableView to reload its data? Does the data source class need to retain a reference to the tableView (through an init)?

    Reply
    • Good question. Yes, that is one way to do it.

      Another is to set the view controller as a delegate of the data source. Whenever the data source updates, it notifies the view controller, which then updates the table view.

      This second way is more generic, because the view controller might have different ways to update the table view. But the first one works too. Maybe not the “purest” but quicker and I use it myself sometimes (you can always switch to a more complex solution later)

      Reply
  6. Quick question:

    I usually will have an observer for the model object in my cell and do something like
    cell.model = modelobject.

    I find this is more convenient if my model has a number of attributes. What do you think of this?

    Reply
    • It might be convenient, but what you are doing is tie your cell (which is a view) to your model or your controllers. Views should be independent and not observe behavior, otherwise you cannot reuse them.

      Also, observers are prone to firing many times. Sometimes this is not a problem, until it becomes one. The moment you write code that should be executed only once, or that depends on other circumstances, observers start to cause weird problems in your app.

      Reply
      • I think this will not affect the reusability. It’s almost same, you are already creating variables for quote like , quoteLabel, author,…etc
        So the cell is only used here to display quote cell, why to declare 10 quote attributes instead of declaring a single quote object in the datasource?

        Reply
        • That works, but only when you use a cell only for a specific type in your app.

          If you reuse the cell in another table view to show a different type, then you cannot tie it to a single model type. Same if you want to reuse the cell across projects.

          That happens, admittedly, only for complex cells. For simple ones, you can get away with simple solutions.

          But reusability is not the only problem. If a cell is tied to a model type, changing the latter breaks the former.

          For a deeper discussion, check this other article: https://matteomanferdini.com/mvvm-pattern-ios-swift

          Reply
  7. Thanks for the tutorial, very helpful.
    I am wondering what is your opinion about using willDisplay instead of cellForRowAt. There are several articles claiming that cellForRowAt (which is a UITableViewDataSource method) should only return the right instance of cell, while willDisplay (which is a UITableViewDelegate method) should be the place to do the configuration.
    And another question: if the model for each cell is mutable, what are your recommendations? For example, if I have a UITextField inside a cell, what is the best way to update the corresponding model since the view knows nothing about it?

    Reply
    • I don’t agree with this.

      I don’t see any benefit to spread the cell configuration code around separate objects, especially when you don’t have a delegate at all. The delegate should be its own separate class as well, so you would have to add a class just for cell configuration.

      Moreover the data source has direct access to the data to do the configuration, while the delegate does not necessarily do so. To remove UI code from the data source I adopt the MVVM pattern instead.

      Regarding model mutability, this is a quite common question. I definitely would not update it from a cell, which is a view and should not know about any model type.

      What I do is create a custom delegate protocol for my cell class and set the data source as the delegate, which again you can do when you configure a cell. Then, when the text field is updated, the cell notifies its delegate, which can take the appropriate steps. And again, the data source has access to the data, so it’s the right place.

      You can see that also in this case, if you configure a cell from the delegate, this gets too convoluted.

      Reply
      • My problem with creating a delegate (regarding model mutability) is that when I am in QuotesDataSource and implement my custom protocol’s method, I have no way to get the appropriate model. Let me explain better:

        * I created a QuoteCellDelegate with one method: func somethingDidHappen(_ cell: QuoteCell) which should be called when something happens inside the cell, let’s say a textfield gets edited, a switch is tapped etc
        * In QuotesDataSource.swift, inside cellForRowAt I set the cell’s delegate: cell.delegate = self
        * I conform to the protocol: extension QuotesDataSource: QuoteCellDelegate

        Now when I implement somethingDidHappen, I only have a reference to the cell. Since I have no reference to the tableView, how am I supposed to get the index in order to find the corresponding model and update it? If I had a reference to the tableView, I could have used tableView.indexPath(for: cell) but do you think it’s right to have a reference of the tableView within the dataSource?

        Thanks!

        Reply
        • That would be one solution, but you are right, it creates an unnecessary connection to the table view, which at this level is not optimal.

          What you have to do is store the index path somewhere. There are two ways.

          The first is to pass to pass the index path to your cell when you configure it, along the rest of the data. Then you can re-define your somethingDidHappen(_ cell: QuoteCell) method into somethingDidHappen(at: indexPath: IndexPath).

          The other way is to implement a table view delegate. This requires that you implement the UITableViewDelegate protocol. Every time a cell comes on screen, the tableView(_:willDisplay:forRowAt:) method gets called, where you can set the delegate. Of course you still need a reference to the table view to get the index path if you go this way.

          Reply
          • In my opinion, when you get the important architectural ideas right (separate data source, etc.) you can afford a bit of freedom in picking a solution for such complex cases.

            So, wether you pass a table view reference or store the index path in a cell, there is not a huge difference. In the end writing code is often a matter of choosing the trade off we prefer.

  8. Interesting topic.

    I met next problem.

    I have tableView with collectionView in each cell. (horizontal scrolling)
    So, when I scroll the collection to fast and then scroll the table – on collectionView reuse it’s shows deceleration for different collection. (data displays correctly, according dataSources, just deceleration and offset for collection wrong)

    Ideas that I heard for this:
    1. Strong references for cells – bullshit (it kills reuse)
    2. Storing offset position for each collectionView on disappearing – ( it’s more realistic but it arise a lot of cases to handle)

    Maybe any other solutions?

    Reply
  9. Hey Matteo! I started reading your tutorials and as I found them really helpful I started refactoring some code in my projects. I reached this tutorial and I moved the TableDataSource as you suggested in an external class instead of using the viewController. The problem is that `tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)` is not being called when I set the dataSource of the tableView as a instance of the external class, but it is called if the viewController performs the protocol. Do you have any idea of why this could be happening?

    Reply
    • Were you using a UITableViewController? One of the few things it does is call reloadData() on the table view. Now you have to do it yourself. Place it after you assign the data source to the table view.

      Reply
      • I was using a normal UIViewController and I had the call to tableView.reloadData() right after the tableView.datasource = cellDataSource. With a let cellDataSource = CellDataSource(list: objects)

        Reply
        • It seems from the last line that you are not putting your data source in a property of the view controller, but just declaring it in place. If you don’t keep a strong reference to it, ARC will get rid of your data source object immediately. A table view only has a weak reference.

          If you look in my code above, I assign the data source instance to self.dataSource

          Reply
  10. I am trying to load quotes into the table cells. What has behooved me is that

    self.quoteSuggestionsTable.dataSource = SuggestedQuotesDataSource(suggestions: self.searchResults)
    self.quoteSuggestionsTable.reloadData()

    print(“Matching items = (self.matchingItems.count)”)
    print(dump(self.quoteSuggestionsTable.dataSource))
    print(dump(self.searchResults))

    the upper line prints out nil. But that can’t be right because the line below that prints all the data for the cells. Commenting out the reloadData() does not make a difference. These lines are in a function inside the View Controller. I have searchResults[SuggestedQuote] = [SuggestedQuote]() defined up in the top of the View Controller. What lines must I add or change?

    Reply
  11. You mention in the final section that

    ‘Be aware that creating this data in the view controller, as I did here, is also not correct.

    Usually data retrieval or creation goes into a separate model controller.’

    could you explain how we would do that in this example for completeness.

    Reply
  12. Great article.

    I have a weird issue though.

    With an external dataSource – setting the tableView.estimatedRowHeight and tableView.rowHeight does not populate the tableView. But when I don’t set them the table view populates as normal, the only issue is that the cell does not expand to fit the text being set to the label.

    This is what I have for an external dataSource:

    var dataSource: MenuItemDataSource?

    viewDidLoad() {
    self.dataSource = MenutItemDataSource(mentuItem: item)
    tableView.dataSource = dataSource
    tableView.reloadData()
    }

    I’m also using a xib for my custom cell. And the autolayout constraints are as indicated on the guide.

    Any idea what could be causing the tableView not to populate when setting the estimatedRowHeight and rowHeight?

    Reply
      • Ok, I figured it out.

        It was my error. In my cellForRowAt() I have a switch statement based on the indexPath.row. After the switch statement I was returning a default cell that I set when initially setting up the dataSource to make the error of not returning a cell go away.

        What’s strange is that it wasn’t returning the default cell then I wasn’t setting the estimatedHeight and rowHeight.

        Reply
  13. I am beginer. I placed the DataSource to separate file as you suggested. But don’t know how to “listen the click event & send clicked item to DetailViewController”.

    class PicturesDataSource: NSObject, UITableViewDataSource{

    }

    inside PicturesDataSource “storyboard” & “navigationController” are not available & therefore navigationController?.pushViewController(detailViewController, animated: true) is not working.

    If I use ViewController as DataSource then code below will work but inside PicturesDataSource it’s not working

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    //1 – try loading the “Detail” view controller and typecasting it to be DetailViewController
    if let detailViewController = storyboard?.instantiateViewController(withIdentifier: “PictureDetailView”) as? PictureDetailViewController {

    //2 – success! set its selecteImage property
    detailViewController.selectedImgName = pictureModels[indexPath.row].pictureName

    //3 – now push it onto the navigation controller ?
    navigationController?.pushViewController(detailViewController, animated: true)
    }
    }

    Reply
  14. I have separated my DataSource in it’s own class but now I have a problem with the function: tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath). In this function I call the performSeque which is not available anymore when you move it out of the UIViewController class. How can I solve this ?

    By the way, excellent workshop !

    Reply
  15. Great article. I have a question.

    If I want to subclass this class:

    class ViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!


    How do I connect ‘tableView’ outlet?

    Reply
  16. Thanks for good article.
    I think you should correct in:
    “So, for example, the third element in the fourth section would have an index path of [4, 3].” Instead [4, 3] should be [3, 2]. Count started from 0.

    Reply
  17. Nice post. Thanks. Can you please answer the following questions?

    – What would you suggest when creating some kind of data filling form screens e.g. UserRegistrationViewController.

    – And while creating a screen that has many different kinds of views and requires scrolling. Would you still suggest to use table view with different prototype cells for each kind of view or scroll view with different subviews?

    Thanks in advance.

    Reply
  18. IMHO it would be better if IBOutlets not force unwrapped.
    I prefer this way:
    @IBOutlet weak var textLabel: UILabel?
    It is more robust way. If you delete label from xib and forget delete IBOutlet from code, your app is not crash.

    Reply
  19. Amazing website with every detail inside it + you include ur xcode project, really amazing, keep it up, you are so great

    Reply
  20. This is the best article on one of the most versatile and popular app elements i.e table views. The article is so thorough that I can confidently say I have an excellent understanding on table views to move ahead into advanced topics . Cant thank you enough.

    Reply

Leave a Comment