Table views in iOS with Swift: a comprehensive guide (cells, data sources and MVC architecture)

Table views in iOS with Swift

Table views are a fundamental piece of almost all iOS apps.

Still, most developers get them wrong, especially using Swift.

This happens in two different ways:

  • some developers don’t even know table views exist or don’t know how to correctly use them. I have even seen people call them “list views” which is not the correct name in iOS.
  • many developers cram data source and delegate code inside view controllers, making them bloated and unmanageable

A couple of years ago I joined a project at the last moment to fix the last bugs in an app.

It soon became clear though that these were not simple bugs: the code of the app was completely wrong.

What do I mean by completely wrong?

In the end, my client had to throw away all the code and hire a second company to rewrite the whole app.

That kind of “completely wrong”.

Of course, they also made their client unhappy because of the extra delay.

Finally, they spent all the budget they had left for the app, making no profit after all the time they spent on the app.

Ouch.

But what caused such serious problems that they had to start from scratch?

I wrote this guide to address all these problems.

Here is what I will cover:

The most common table view mistakes iOS developers make

There were many architectural mistakes in that app, which were evident even after a superficial inspection.

But what struck me the most were the how even basic things were wrong. Many standard UIKit classes were not used the way they are supposed to.

The most glaring mistake was the complete lack of table views, even if the app had many screens showing lists. This is the typical place where you use a table view in iOS.

To my surprise instead, the developer used a simple scroll view.

What made thing even worse was that each view inside the scroll view had a view controller attached. This showed that the author had no idea what view controllers are for.

I already wrote about the role of view controllers in iOS in the past, which explains why this was a mistake.

What surprises me, again and again, is that many developers seem to ignore the existence of table views, or they use them in a wrong way.

Many developers seem to opt for simple scroll views when they should use a table view or a collection view instead.

And even when they pick the right option, they bloat view controllers with data source and delegate code.

This has to stop.

You need to get the fundamentals right if you want to be a professional iOS developer. Or even if you want to create apps on your own.

So in this article, we will have a look at:

  • when to use scroll views, table views or collection views
  • how table views work
  • how to create and reuse table view cells
  • the correct architecture to use and where to place the code for the table view data source

When to use scroll views, table views or collection views in iOS

If you are serious about iOS development, you should get familiar with Apple’s iOS Human Interface Guidelines ASAP.

These not only help you creating the right interfaces for a mobile app but also tell you clearly which iOS component you should use for each task.

Just looking at the images on some sections of this guide, it is clear what each component is meant for.

Let’s start with scroll views:

scroll views human interface guidelines

iOS scroll views are views that make content scrollable, horizontally, vertically or both.

Their content is unstructured, which means there are no repeating elements or patterns.

When we look at table views, the difference is clear:

table views human interface guidelines

Table views are for vertical lists of repeating elements.

Here the word vertical is important. Table views are only used when the list of elements is arranged in a single column of elements.

There is no such thing as a horizontal table view. For any horizontal arrangements of elements, you need a collection view, even if that collection only scrolls vertically:

collection views human interface guidelines

In fact, both table view and collection views are scroll views. The UITableView and UICollectionView classes are both subclasses of UIScrollView.

So why should we use table views and collection views over scroll views?

Advantages of table views over scroll views

Table views and collection views are made for repeating, discrete elements.

But in fact, I recommend using them for almost any type of scrolling content, even if it looks like such content is neither structured nor repeating.

To be honest, the images in Apple’s HIG don’t do table views much justice. You can use them for much more than the simple ones you see in Apple’s examples.

Have a look at this small collection of table view designs I put together on Dribble:

table view designs collection on dribble

In this article, I will only cover table views, but collection views are even more flexible, so they share the same advantages and allow you to create more complex layouts than table views.

So, let’s see what advantages table views offer over scroll views.

  • Table views automatically calculate the size and position of every item. If you use a scroll view, you have to calculate the frame of each view. Even if you use Auto Layout, you still have to create all the constraints yourself, which are trickier to get right in a scroll view than in a normal view. This also produces a lot of boilerplate code.
  • Table views keep the memory footprint of your app small. The views inside a normal scroll view are kept in memory all the time. This becomes a problem when the content grows in size. When you have a lot of views, memory fills quickly and when an app takes too much memory, it gets terminated by the system.
  • In a scroll view, you need all the content at once. The alternative is to write your own code to manage the placement of views on the screen and the loading and unloading of views that are not visible. Table views do all this for you.
  • Table views make scrolling smoother (if you implement them the right way). Views in a scroll view stay in the view hierarchy unless you remove them in your code. This means that their position gets constantly recalculated when scrolling, even if they are outside of the screen. This leads to slow scrolling and power consumption. A table view solves these problems because it reuses its cells and takes them off the view hierarchy when they are outside of the screen bounds. A table view only needs a few cells at any given time.
  • Table views allow you to reload, insert, delete and reorder items. All these can also be done with custom animations and reloading can even be done for single elements. Doing this in a scroll view is very complicated and you are left implementing it all by yourself.
  • In a table view, you can divide items into sections and use indexes. The section headers remain on the screen when scrolling, something that is not easy to implement. And indexes allow you quickly jump ahead in long table views.

For these reasons, I recommend using table views for almost all kind of scrolling content.

If your content does not have repeating elements, your table view will not enjoy reusing cells. But you still get all the other benefits.

Scroll views are still useful in some cases though. For example, when you want to display something with a wide area that can be scrolled in different directions or zoomed. Think of photos or maps.

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

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

The cells of a table view are instances of or subclasses of UITableViewCell, which is itself a subclass of UIView. The table view takes care of adding cells to, removing cells from and vertically arranging cells in the view hierarchy.

When a cell goes out of the screen because of scrolling, the table view can reuse it to display a new item. In this way:

  • performance improves because we avoid the extra overhead of removing a view from the hierarchy, instantiating a new view in memory and adding it to the view hierarchy. It’s far more efficient to just move an existing subview to a new location;
  • the memory footprint is kept low because there are always only a small number of cells at the same time in the view hierarchy and in memory.

how a table view reuses cells

The table view does all this automatically and does not need any extra calculation on your part like a simple scroll view would.

All the cells in a table view are created from prototype cells, so they are all like each other. A table view can have just one prototype cell if it displays a homogeneous list of elements or more than one for heterogeneous lists.

For example, in a social network app where you can publish text posts, pictures, and videos, a table view would have a prototype for each one of these types of items.

This allows the table view to reuse a cell for another element of the same type. A table view with a thousand elements might need to instantiate just 4-5 cells to work.

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

It might seem intuitive that, for a table view to work, you would need to configure it with the list of all the elements at once.

But to further optimize memory use and performance, a table view does not need all the elements at once.

In this way:

  • you don’t have to retrieve all the elements at once, which might be a long and expensive process that would slow down the app;
  • you don’t have to keep all the elements in memory at the same time, which would increase the memory footprint of the app.

Both of these might be useless and you might waste resources to load elements that are very far down the list and the user would never see.

Let’s see how this works.

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

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

a table view needs a delegate and data source

These two protocols specify all the methods a table view might call to perform its functions.

There are two protocols instead of just one because the methods they contain are different in nature:

  • The data source provides data to the table view. It tells the table view how many sections there are, how many items there are in each section, and provides the elements and the titles for section headers and footers;
  • The delegate manages layout and manipulation of elements. This includes specifying height or indentation of cells and coordinating selection, insertion, deletion and reordering of the elements.

How a data source provides data to a table view

A basic table view that only displays a list of items and does not allow any interaction, needs only a data source.

The UITableViewDataSource protocol has only two required methods, that you must implement to make the table view work:

  • tableView(_:numberOfRowsInSection:), which tells the table view the total number of items in the list. The table view uses this information to prepare its scrolling area and to display a scroll bar of the appropriate size.
  • tableView(_:cellForRowAt:), which configures and returns the cells to the table view.

You can have a simple working table view by just implementing these two methods.

One thing you have to keep in mind though is that there is no guarantee of how many times they will be called.

  • The tableView(_:numberOfRowsInSection:) method gets usually called when you load the table view the first time and any other time you alter the table view content. This method is called once for every section in the table view, so if there is only one section, this method is called once.
  • The tableView(_:cellForRowAt:) method gets called whenever the table view needs a cell for an element on the screen. This means that even if a table view might contain thousands of elements, this method only gets called for the ones that are visible on the screen at the moment.

data source calls sequence from a table view

As the user scrolls through the whole content, these methods get called again for the elements and the section as they come on the screen.

A table view does not remember such information, even if it asked for it already. This means that these methods can be called for the same element or section more than once. Keep this in mind.

The architectural problems of table views, cells and data sources

There are many architectural mistakes iOS developers make when working with table views.

These are all related to misunderstanding how the MVC pattern works.

As I discussed in this other article, the MVC pattern has four layers, and not three:

  • the model
  • model controllers
  • view controllers
  • views

According to this model, we can see what are the common mistakes related to table views.

  • A view controller is often both the data source and the delegate of a table view. This makes the code in a view controller grow very quickly, which is a quite common problem in iOS apps. It is not the responsibility of the view controller to handle data for a table view. The data source and the delegate should be model controllers instead.
  • Cells get configured through their outlets or, worse, using view tags. You can find such code even in Apple’s example. External objects should not know about the internal implementation of a cell because this introduces coupling between types. A cell should instead expose properties with simple types to enable the passing of data.
  • The data source passes whole model objects to cells. This is usually done as an alternative to the previous point but still, has a problem. Views should not know anything about the model. This also introduces coupling and makes a cell less reusable.

Simple view controllers are better than table view controllers

As an example for our working table view, we will create a small app that displays a list of quotes.

All the cells will look the same, showing the author of the quote and the text.

First, we will start laying out the user interface in Interface Builder. We need a view controller for this and luckily any Xcode project comes already equipped with one.

We also want to have the navigation bar at the top of our screen. Our example app has no navigation, but a more complex app would.

But even without navigation, the app looks better with a top bar with a title.

To have a navigation bar at the top of the screen, we need to use a navigation controller.

This is because simple view controllers don’t have navigation bars. The navigation bar belongs to the navigation controller. I talked about this more in another article on the principles of iOS development.

app flow with a navigation controller in the storyboard

We could use a table view controller instead of a normal view controller, but I don’t recommend it.

The UITableViewController class (which is a UIViewController subclass) was created by Apple to automate a few common tasks. Although this removes a bit of boilerplate code, I personally don’t use table view controllers.

  • A table view controller uses its table view as its main view. This might not be a problem initially until you need to add other kinds of views to a screen.It is actually quite common to do so, for example when you want to overlay some views to the table view to add buttons or other UI elements. You cannot add these views as subviews of the table view though, and with a table view controller, this is the only view you have. So you usually end up replacing the table view controller with a normal view controller.
  • A table view controller sets itself as both the delegate and the data source of the table view. As I explained in the previous section, this is an architectural mistake.

Finally, the UITableViewController class does not really do much, so there is not much we lose anyway.

If you still want to use a table view controller, one option is to add it as a child view controller of another view controller.

I honestly don’t see much advantage in doing that, but it might be useful to organize code in a much more complicated app.

When using a standard view controller, we have to add our table view to the interface manually.

We want this table view to cover the whole screen, so we have to set Auto Layout constraints to pin its edges to the ones of the view controller’s view.

constraints to pin the table view to the edges of the view controller

I will not go over setting Auto Layout constraints in detail since I covered this in my example of the fundamental concepts of iOS development.

Laying out auto resizing table view cells with Auto Layout

We now need a prototype for the cells of the table view. All the cells will come from this prototype.

First of all, we have to add one prototype cell to our table view in the inspector panel:

adding cell prototypes from the inspector panel

We then can create the interface of our cell prototype.

Our cell will need two labels: one for the quote and one for the author.

cell prototype with labels

The quote label needs to have more lines and expand vertically to adapt to the text of the quote, since different quotes will have different lengths.

For this to happen we have to set its number of lines to be 0 in the inspector panel. Zero here means that the label does not have a fixed number of lines but will expand to adapt to its text.

setting the number of lines of a label in the inspector panel

Placing labels in our cell prototype is not enough, though.

We know that cells will have to adapt to the width of the screen on different devices and to the height of the text of the quote.

Horizontally we set constraints to pin the edges of our labels to the edges of the cell. In this way, as the cell adapts to the screen width, the labels take their space in the cell.

Vertically we will do the same, but it will have another effect.

Since a cell is not constrained vertically by any edge, our constraints will “push” the cell to expand as the content of a cell grows.

This is how you should set the auto layout constraints in the cell prototype:

pin constraints for the labels in the cell prototype

We will have to do one more step in our code to make this work. We will see that in a moment.

Use custom classes for your table view cells to properly arrange code according to the MVC architecture

As we have seen above, this cell is not a plain UITableViewCell, so it needs its own class if we want to properly architect our code.

First, create the class in a new Swift file. This class needs to be a subclass of UITableViewCell.

Then set the class for the prototype cell in Interface Builder.

setting the cell prototype class in interface builder

We are not done yet. Cells in a table view need a reuse identifier.

This will be used by the table view to recycle the cells that go out of the screen. This identifier is just a string and can be anything you want.

I usually use the class name, since this helps me avoid misspelling it in my code.

setting a cell prototype reuse identifier in interface builder

We now have to connect our labels to outlets in the class, to be able to set their text from our code.

If you don’t know how to do this, you can check my other article that shows it step by step.

Now, many would stop here and use these outlets to set the text in the labels from outside of the QuoteCell class (doing it inside of the data source).

This works and you can even find in in Apple’s Table View Programming Guide, but it’s not the correct way.

This is because the data source would be tied to this specific implementation of the quote cell.

What if later we want to change how the cell displays the author and the text? What if we remove these outlets completely?

This would break also the code in the data source.

Always remember that changing the internal implementation of a class should not affect any other external class.

What we want instead is the data source to only know that it has to pass the author and the quote to the cell as simple strings.

What the cell will do with these strings is not a concern for the data source.

To achieve this we have to create two more properties in the QuoteCell class, for the author and the quote text. Both properties have a simple QuoteCell class, for the author and the quote text. Both properties have a simple String type.

We then add a property observer to both, to set the text in the corresponding label whenever each property gets set.

If you want to use a more advanced solution, you can use the MVVM pattern, which is a variant of the MVC pattern.

I cover how to this in detail in my Ultimate Course to Making Professional iOS Apps

Creating the model of the app and populating the table view through a data source

We now need a model class for the quote.

There in not much needed for our example here: just a struct that carries the author name and the quote text.

We now need the data source for the table view. As we discussed above, we need a separate class for it which implements the UITableViewDataSource protocol.

This is why.

This protocol has many methods, although we only need two of them to make our table view work. All These methods would make the code of the view controller grow too much if we put them there.

Moreover, the UITableViewDataSource protocol defines a lot of functionality that should not be a concern of a view controller in the first place.

The data source needs to receive the quotes that will populate the table view. This is the only things that it needs in our example.

We simply pass an array of quotes to the data source through an initializer:

We now need to implement the two required UITableViewDataSource methods.

Our table view will identify:

  • sections by an integer index
  • elements by the index of the section that contains them and the index of its position in the section

index paths of sections and elements in a table view

Since elements are identified by a pair of indexes, table views use the IndexPath class to represent such pairs.

So, for example, the third element in the fourth section would have an index path of [4, 3].

If a table view has no section separations, like in our case, it is assumed to have only one section at index 0.

Implementing the required data source methods for a working table view

The first required method of UITableViewDataSource is tableView(_:numberOfRowsInSection:).

As the name suggests, this method tells the table view how many elements there are in total in a section.

This method is straightforward to implement since it just needs to return the count of the quotes array. We ignore the section parameter because it is always 0 since our table view has only one section.

I usually use Swift extensions to adopt protocols in my classes, which makes my code more organized and readable.

The second required method is tableView(_:cellForRowAt:).

Through this method, the table view requests a cell for an element at a specified index path. This method has to return a fully configured cell for the table view to use.

The first thing this method needs to do is to request a cell from the table view.

This is done through the dequeueReusableCell(withIdentifier:) method of  UITableView. This method returns:

  • a reused cell, if there is any available in the table view for the specified identifier (a cell that went out of the screen while scrolling), or
  • a new cell, created from the prototype for the specified identifier

After this, we just need to retrieve the quote for the row in our array of quotes and configure the cell before returning it.

As you can see, there is not much code in this method.

This is good and is exactly how it should be. We spread responsibility into the appropriate classes instead of putting all the code in the data source.

Connecting all the parts to have a working app with the correct architecture

Up to this point, we created all the separate parts of our table view architecture:

  • the view controller that contains the table view
  • the cell prototype for the quotes in the list
  • the cell class to keep the view code isolated and reusable
  • the model struct for our quotes
  • the data source to populate the table view

We now have to connect all of them together.

The central connection point will be the view controller.

In the MVC pattern, this is exactly the role of a view controller: acting as a glue object that passes data between the other layers of the MVC architecture.

First, we create an instance of our data source in the initializer of our view controller. We pass this instance some quotes we hard code here for simplicity.

In a real app, these quotes might come from a file or from the network. We covered a lot of concepts already in this article, so I kept this simple.

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.

Finally, we configure our table view.

We do this in the viewDidLoad() method when all the user interface has been loaded and the table view is available. See my article on the lifecycle of a view controller for more details.

Here we did the following:

  • set an estimated row height and the UITableViewAutomaticDimension constant for the rowHeight property. This is the final step to allow Auto Layout to calculate the height of the cells dynamically. For the estimated row height just pick the height of the cell prototype from Interface Builder. Each cell’s height will be different at runtime, but this is a reasonable estimate.
  • pass our data source instance to the table view.
  • load the data in the table view through the reloadData() method. You call this method every time you update the data in your data source and you want the table view to update its cells.

We can now run the app and see our quotes inside the table view.

final app screenshot

Summary

We have seen in this article how a table view works and how to configure it. This is a simple example but already involves different classes and concepts.

You can get the whole project for this app on Github.

With table views it is important to understand:

  • What we need them for: table views are for displaying vertical lists of repeating elements, but we can use them for any kind of content. Scroll views are instead for more generic scrolling that does not follow specific structures.
  • How they actually work: table views display elements through cells that are reused, so that the memory footprint of the app remains low and the scrolling smooth.
  • How to configure them: table views don’t get their data directly. Instead, they ask the information only at the moment they actually need it. This is done through the data source.
  • How to keep code separated and in the appropriate classes: a lot of code related to table views needs to be in different classes:
    • code related to visual appearance should go in the cell class;
    • the UITableViewDataSource and  UITableViewDelegate protocols should be adopted by separate classes and not by the view controller;
    • data representation goes inside the model;
    • retrieving data goes into a separate class as well;
    • the only role of the view controller is to put all these pieces together.

When these concepts are clear, it becomes easier to implement the rest of the UITableViewDataSource or UITableViewDelegate methods, in case you want to add more functionality to your table views.

Further steps and reading

This is a simple example and indeed it might be all that is needed in a simple app. But it’s likely that you will want more out of your table views in a real world app. These are a few common paths you might want to explore:

  • Loading the quotes from a file
  • Selecting a cell and navigating to a detail view controller
  • Where to place the code that creates model controllers
  • Adding a separate view controller to add new quotes
  • Updating the table view when the data changes

You can find all this in my free course on building professional iOS apps

Some useful Apple guides:

Learn more about table views, app architecture and how to build professional iOS apps in my free course

100% privacy. No games, no spam. Unsubscribe at any time.

Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+
  • zhongkai li

    learn.before reading this, i use a wrong way about datasource and extension.

  • alitsnr

    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 we would create in another place? Where we would create that that data instead of inside of view controller?

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

  • Nolan Regier

    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

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

  • Thomas Richardson

    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*

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

      • Mark Moeykens

        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.

  • Gabriel J. S. Oliveira

    In which class exactly the Struct Quote should be in? Cell or DataSource?

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

  • Gabriel J. S. Oliveira

    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?

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

  • Duane

    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)?

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

      • Duane

        Thank you for the quick response. I actually did as you suggested and created a delegate.

  • JoshD

    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?

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

  • Thom Thom

    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?