The Correct Way to Display Lists in iOS and What Many Developers do Wrong

gears

I am always surprised by how some developers get wrong some basic aspect of iOS development.

A couple of years ago I joined a project at the last moment on an app that had to be delivered shortly but still had a lot of bugs. As I investigated the bugs, though, it became clear that they were not simple bugs: the code of the app was completely wrong. Eventually the company that hired me had to hire a second company to completely rewrite the app. Their client was not happy and they spent all the budget they had left for the app.

What do I mean when I say that the code of the app was completely wrong? There were many huge mistakes, which were evident even at a superficial inspection. The architecture of the app was a mess, but what struck me more were the basic things. Many standard UIKit classes were not used the way they were supposed to.

The most glaring mistake was the complete lack of table views. The app showed lists of cities and things to do in them.  This is the typical example where I expected this a table view. To my surprise instead, the developer used a scroll view filled with normal views. To make the matter worse, each view had its own view controller, showing that the author had no idea what view controller were 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. This was indeed not the first time I saw something like that and definitely not the last. Many developers seem to opt for simple scrool views when they should use a table view or a collection view instead.

These are fundamental components of iOS that and they are often needed. So, let’s have a look in more detail at when to use table views and collection views, as well as a practical example to show the correct way to work with them.

When to use scroll views and when table views or collection views are more appropriate instead

iOS scroll views are views that make their content scrollable, both horizontally and vertically. Table view and collection views are in fact scroll views themselves (the UITableView and UICollectionView classes are both subclasses of UIScrollView).  When should we use the former and when the latter?

In short, table views and collection views should be used any time you have a scrollable list of repeating items. Table views are used for vertical lists. Collection views expand on the capabilities of table views and add horizontal scrolling, grids and more complex layouts. The items in table views or collection views are often of the same kind but can also be of different ones. Table views and collection views also offer the ability to group items in sections. Both table views and collection views follow the same principles, so I will only talk about table views in the rest of this article.

Now, why should you chose a table view instead of just using a scroll view? There are different reasons:

  • If you use a scroll view, you have to calculate the positioning of each item, even if you use Auto Layout. This produces a lot of boilerplate code, so Apple created table views to cut such code.
  • If you want to add, inserting, deleting or reordering, there is even more code you have to write by yourself. Table views provide also this functionality for free.
  • The views contained inside a normal scroll view are kept in memory all the time. This becomes a problem when the list of items grows, which is often the case. When you have a lot of views, memory fills quickly and when an app takes too much memory, it gets terminated by the system. Moreover, these views stay in the view hierarchy. When scrolling, their position gets constantly recalculated, even if they are outside of the screen. This leads to slowness and power consumption. A table view solves these problems because it reuses views, so it needs only a few at any given moment.

For these reasons, every time you need to display a list of items, a table view is the right choice. A scroll view is still useful for other cases. For example:

  • When you have a UI that does not fit on the screen, but it’s not a list of homogenous items.
  • When you want to display something with a wide area that can be scrolled in different directions or zoomed, like a picture or a map.

How table views work

First of all, a table view displays its items using views, which are called cells. The cells of a table view are instances of or subclasses of UITableViewCell (which is a subclass of UIView). These are arranged vertically by the table view. When scrolling causes a cell to go out of the screen, the table view reuses it to display a new item. In this way there are always only a small number of cells at the same time on the screen and in memory.

This is all done by the table view and does not need any extra calculation on your part. All you have to do is reconfigure the reused cell to display a new item.

All the cells in a table view  are created from a prototype cell, so they are all similar. A table view can also have more than one prototype cell, if it needs to display more than one kind of element. For example, if you make a social network app where you can publish text posts, pictures or videos, a table view will have a prototype for each one of these type of items.

It might seem intuitive that, for a table view to work, you would need to configure it with the list of all the elements. But to avoid keeping all the items in memory, table views work differently. All you do is tell the table view how many items there are in total, without providing them from the start. The table view then relies on two external objects, to which it asks information about the cells to display on the screen only when needed.

These two external objects are called data source and delegate. They are must conform to the UITableViewDataSource and UITableViewDelegate protocols but can be of any class. These two protocols specify all the methods a table view needs to perform its tasks. There are two protocols instead of just one becasue the methods they contain are quite different in nature, so it makes more sense to group them in two separate protocols:

  • The data source provides the data to the table view, which includes the configured cell for each element.
  • The delegate manages selections, section headings and footers, and coordinates inserting, deleting and reordering.

A basic table view that only displays the items and does not allow any interaction, needs only the data source . The UITableViewDataSource protocol only has two required methods. The first one is tableView(_:numberOfRowsInSection:), which tells the table view how many elements there are in total. The table view uses this information to prepare its scrolling area and display a scroll bar of the appropriate size. The second required method is tableView(_:cellForRowAtIndexPath:), which configures and provides the cells to the table view. You can have a simple, working table view just by implementing these two methods.

The data source and delegate protocols are separate, but a singe class could conform to both. A common mistake is indeed to make one class be both the data source and the delegate of a table view. Both these protocols have many diverse methods, so this class’ code can grow quickly. To make things worse, this object chosen to be data source and delegate is often a view controller. View controllers in iOS already tend to include too much code they should not have. Being the data source and the delegate for a table view makes them even more unmanageable. Since the data source and the delegate have separate responsibilities, they should be two separate objects, none of which should be a view controller.

Now that we have clear in mind how a table view works, let’s see this put into a practical example.

Implementing a working table view

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.

Setting the table view in Interface Builder

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 could use a table view controller instead of a normal view controller. The UITableViewController class (which is a UIViewController subclass) was created by Apple to automate a few common tasks that are often required when using a table view. Although this removes a bit of boilerplate code, I personally don’t use table view controllers and rely on normal view controllers instead.

The first reason for this is that a table view controller uses its table view as its main view. This might not be a big problem for a while, until you need to add other views to the screen that are not part of the table view. 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 removing the table view controller to use a normal view controller with a normal view, to which you can then add the table view and any other view you need.

Another reason not to use a table view controller is that when you look at what the UITableViewController class actually does, it is not much. So the benefits are not as big as one could immagine. Moreover, one of the behaviors of UITableViewController is to set itself as both the delegate and the data source of the table view. As I said above, that is a behavior we don’t want.

So there is not much we lose of we don’t use a table view controller. Setting up a table view without it is still simple and straightforward anyway, so I never use one.

Since we have decided to use a standard view controller, we have to add our table view to its interface. 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.

Table view constraints

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 the prototype cell with Auto Layout

We now need a prototype for the cells of the table view. All the cells will come from this prototype. First, we have to set out table view to have one prototype we can customize:

Prototypes

We then add a label to our prototype cell for the name of the author of the quote. This label only has one line and is set to have his top and both his sides pinned to the edges of the cell. This is needed so the label will expand horizontally with the cell on devices with a bigger screen.

Author constraints

Now we need to add a label for the text of the quote. This label actually needs to have more lines and expand vertically to adapt to the quote text, since different quotes will have different lengths. For this we have to set its number of lines to be 0, which indicates that the label does not have a fixed number of lines but will expand to adapt to its text. Then we have to pin its top to the author’s label bottom and its sides to the edges of the cell, like we did for the previous label. In this way the only direction in which this label can expand is down.

We also need the cell to expand to adapt to the size of this last label. In the past, setting the height of table view cells dynamically was a pain. Luckily it’s now possible to do it with Auto Layout, so that the height of the cell will be calculated automatically.

To do this, first set the height of the label manually to be at the desired distance from the bottom of the cell. Don’t worry if you are making the label longer or shorter than the text it contains. This is just some dummy text anyway and the real one will be set at runtime, making the label get the right height. What is important here is to set the right distance from the bottom of the label to the bottom of the cell. When you have set it, pin the bottom of the label to the bottom of the cell with a constraint. This will allow Auto Layout to calculate the height of the cell. We will have to do one more step in the code to make this work. We will see it in a moment.

Quote constraints

Creating a custom class for the table view cells

Since this cell is custom and not a plain UITableViewCell, it needs its own class. First, create the class in a new Swift file. This class needs to be a descendant of UITableViewCell.

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

Cell class

We are not done yet. Cells in a table view need a reuse identifier. This will be used to allow 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 mispelling it in code.

Reuse identifier

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 from the outside of the cell to set the text in the labels (doing this inside the table view data source). This works and you can even find in in Apple’s sample code on table views, but it’s not the correct way. This is because the data source would then 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, removing these outlets? 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 only knowing that it has to pass the author and the quote to the cell as simple strings. What the cell will do with these stings 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 property have a simple String type. We then add to them a property observer that sets the text in the corresponding label whenever each property get set.

Creating the model 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 to create the data source for the table view. As I said, create a separate class for this. The UITableViewDataSource protocol has many methods, although we only need two to make the table view work. All ehese methods can make the code of the view controller grow a lot. Moreover, this 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 pass them through an initializer:

We now need to implement the two required UITableViewDataSource methods. The first one, tableView(_:numberOfRowsInSection:), tells the table view how many elements there are in total. This method is straightforward since it just needs to return the count of the quotes array. I usually use Swift extensions to adopt protocols in my classes, since this makes them more well organized and readable.

The second required method is tableView(_:cellForRowAtIndexPath:), which returns a configured cell for a requested row of the table view. The first thing this method needs to do is to get a cell from the table view. This is done through the dequeueReusableCellWithIdentifier(_:) method of UITableView. This method returns a reused cell, if there is one available, or a new one created from the prototype otherwise.

After this we just need to retrieve the quote for the row and configure the cell before returning it.

As you notice, there is not much code in this method either. This is good and is exactly how it should be, because we spread responsibility into other classes as well.

Putting all the pieces together

The only thing we have left is to connect all the pieces together in the view controller.

First, we create an instance of the data source in the initializer of the view controller, with some quotes to display on screen. In a real app, these quotes might come from a file or from the network. The important thing though is that the data source itself should not be concerned about retrieving data. Its only concern should be to configure the table view.

Be aware that creating this data in the view controller, as I did here, is also not correct. I just did it to keep this example simple. Usually this code would go into some other object, which only responsibility is to retrieve data or maintain state.

What we have left is to configure the table view. We do this in the viewDidLoad() method, when all the user interface has been loaded and the table view is available. First, we have to pass it our data source instance. We also have to 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 dinamically, as I mentioned above. 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 that is a reasonable estimate.

I also put superclass overrides in a separate extension to organize my code better:

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

Final result

Conclusions

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 many developers get wrong. With table view it is important to understand:

  • What we need them for: table views are for displaying lists of repeating elements. Scroll views are instead for more generic scrolling that does not follow specific patterns.
  • How they actually work: table views display elements through cell that are reused, so that the memory footprint of the app remains low.
  • 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 and the delegate.
  • How to keep code separate 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 and retrieving data goes in a separate class. 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
  • Add a separate view controller to add new quotes
  • Add cell deletion and reordering, implementing more data source and delegate methods

Some of these can be found in my other article on the fundamental concepts of iOS development.

Some useful Apple guides:

Table views are only one of the fundamental concepts you need to make professional iOS apps. Get my free course that covers the other important parts

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.