UITableview: The Essential Solution for Scrolling iOS Interfaces

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.

The 5 most common misconceptions about SwiftUI

GET THE FREE BOOK NOW

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.

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.

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.

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.

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.

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.

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.

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

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.

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.

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.

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.

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.

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.

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.

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

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.

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.

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.

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;

The 5 most common misconceptions about SwiftUI

SwiftUI is the future of UI development. And yet, many developers share some misconceptions that prevent them from understanding how SwiftUI works and from using it in real apps.

GET THE FREE BOOK NOW