Flawless UICollectionViews and UITableViews - Digital Leaves
1211
post-template-default,single,single-post,postid-1211,single-format-standard,qode-social-login-1.0,qode-restaurant-1.0,ajax_fade,page_not_loaded,,select-theme-ver-4.1,wpb-js-composer js-comp-ver-5.2,vc_responsive

Flawless UICollectionViews and UITableViews

Most probably, if you are currently working in an App including tableviews or collectionviews, specially if you are retrieving the data from a backend or REST API, you’ll find yourself with some requirements for the collectionviews and tableviews like lazy loading of images or dynamic cell size.

In many occasions, developers facing this requirements will just use some Lazy Loading library (like SDWebImage or similar), and don’t get me wrong, I have nothing against using frameworks, libraries and Pods, but the last project I was asked to review had 26 cocoa Pods in it. It was a freaking Frankenstein, and I luckily didn’t end up maintaining that code. Sometimes, a few lines of code can save you from including yet another Pod in your project, which would be appreciated by the poor developer in charge of maintaining it later, and will also make you a better developer.

In this post, I will show you how, with few methods and functions, you can get that flawless UICollectionViews and UITableView you always dreamt of.

Dynamic size for cells in the Autolayout world

A good looking collection view has cells aligned evenly in the collection view, with a number of fixed columns per row. For table views, it’s nice being able to show the full content for every row, adapting the height of each cell accordingly. Let’s have a look at how to do this.

Evenly aligned cells in collection views

For UICollectionViews, we would usually want to display a fixed number of cells per row. To do this, we just need to calculate the size of the cells. Taking into account that for n cells we will have n+1 spans (the edges and the space between the cells), we can calculate the size of the cells by dividing the total screen width between the cells and the spans.

calculations

So cellWidth = (screenWidth –  (n+1) * span) / n. We will add a -1 to this value to have a error margin, to make sure we get the same results for every screen. Thus, we will have the following initialization in our UIViewController:

The kLazyLoadAspectRatio will allow us to configure the shape of our cells, from a square (aspect ratio = 1.0) to a rectangular form. If your design has cells of 100(width)x120(height), you would like to set an aspect ratio of 1,2.

We will configure our cells to just set a placeholder image (until we get to the lazy loading section below). The cell size can be specified in a delegate method of UICollectionViewDelegateFlowLayout, so we will add this in our UIViewController and then set the method collectionView:collectionViewLayout:sizeForItemAtIndexPath:.

And just as simple as that, we have a nicely distributed collection view.

Dynamic height row in table views

In the case of UITableView cells, we need to take into account the AutoLayout constraints and how they allows the cells to grow (or not) according to their content. We need to first design our cells (whether in a separate .xib or in the same storyboard) properly, following the following rules:

  1. The “Row height” for the UITableViewCells must be set to “Default”, not custom, in the IB.
  2. All UILabels covering the whole height must have 0 as “number of lines”.
  3. All elements must be pinned to the edges of the cell according to its desired position.
  4. We should not set fixed heights for elements that cover all height of the cell.
  5. We have to be careful with aspect-ratio relationships in images, that could implicitly set a fixed height for a cell when aligned to other elements.

Let’s see an example:

autolayout-cells

Here, the text will define the height of our cell, as it goes top-to-bottom. The image has a fixed height, but as it’s not pinned to the bottom, it will only set a minimum height restriction for our cells. Take into account that if we had, for example, a situation like this:

bad-layout

You may think that we are not setting a strict height, because the aspect ratio will make that the image just gets bigger as the label’s text grows, but in practice, the implicit size of the label together with the image aspect ratio will set an implicit size limit for the cell. We will get an undesired result like this one:

bad-results

So this step is really important. You have to carefully set your AutoLayout constraints to make sure the height will be allowed to grow as the content does.

Now it’s time to indicate that we want dynamic height for the rows in our table view. We will need to specify a “placeholder” size that will be used to initially draw the cell in tableView:estimatedHeightForRowAtIndexPath:. We will, however, define also the method tableView:heightForRowAtIndexPath:, returning UITableViewAutomaticDimension, to specify that we want the cells to dynamically adjust their height. Thus, the cell will initially be configured with the placeholder size and then “adjusted” to fit the AutoLayout constraints.

The size returned in tableView:estimatedHeightForRowAtIndexPath: is not really critical, but the more you adjust to an “average” row height, the more efficient the cell displaying process will be. The final result is a nicely distributed table view with dynamic cell sizes:

nice-tableview

Lazy Loading of images and caching for your cells

Lazy loading is one of the most daunting challenges for newcomer iOS developers. This admittedly has to do with the way UITableView and UICollectionView controls work. In both controls, the displaying of the cells is performed in a pull mechanism, there is a delegate of the UITableView/UICollectionView, and when these views need some information for displaying the cells, the delegate is asked to retrieve it.

Understanding the problem

As you already know, tableView:numberOfRowsInSection: and its collectionview’s equivalent, collectionView:numberOfItemsInSection: allows the view to inquiry the number of rows to show, whereas tableView:cellForRowAtIndexPath: and collectionView:cellForItemAtIndexPath: are called whenever a new cell has to be displayed. However, there is a duality between the cells in the collection or table (which have an NSIndexPath[section-row] to identify its position) and the actual data (which is probably stored in an array with an index). Positions in the data array are not supposed to change, however, the cells are not always displayed in the same position, because they are reused for efficiency.

The cells are kept in a pool where they are dequeued and served as they are needed. When you ask for a cell with dequeueCellWithReuseIdentifier: a new one is created if and only if there’s no previous created cell that can be served. Otherwise, this previously created cell will be delivered. This cell has not been “cleaned” or “zeroed”, so it has the same configuration it had, even thought it’s now associated to a different NSIndexPath (thus pointing to a different data element from the array). This means that you have to re-configure it again, cleaning the previous image contained in the cell. If not, the old image will be shown in the cell.scrollingUp

However, when you lazily download an image from the network, it takes some time for the image to download. During this time, the cell could have changed its position (because the user is scrolling throughout the collection, for example), so the image will be associated to the NSIndexPath that was passed as parameter when the function was initially called.

This situation is depicted in the picture above. Imagine the cell at the top-left showing some hockey players. If the cell starts loading the image (at indexPath {0, 0}) and the user scrolls up before the image is retrieved from the network, when it’s finally downloaded, the indexPath {0, 0} has “moved” and now should show this plant on gray background. Instead, as the asynchronous call is contained in collectionView:cellForItemAtIndexPath: where indexPath is {0, 0}, the image is incorrectly loaded in this cell. Due to the asynchronous nature of a multi-thread download operation (in the 14 cells visible), the result will be indeterministic, with some cells showing correct images, some cells showing incorrect images, and some cells showing no images at all.

The solution

The perfect solution for our image loading problem is a combination of an image cache and a safety check to make sure that we only load the images in their right spots. We will also add a clever trick to further improve the image displaying when the scrolling ends.

We will create a singleton class called ImageManager to take care of the image downloading and caching. We will implement a really simple image cache (just a dictionary with image URLs as keys) with a maximum number of images. More complex caches strategies can be implemented, but this is enough for our purposes:

For downloading images, we will add a method to our ImageManager, as we are already in DIY mode, we will use our good old NSURLSession instead of adding some other Pod. We will check first if we already have the image in our cache, so we can return it immediately, and make sure to store retrieved images in the cache:

Now, with our ImageManager ready, it’s time for us to configure our cells. We will create a method called updateImageForCell:inTableView:withImageURL:atIndexPath:, and call it for configuring our cells. For this simple example, instead of creating a new subclass of UICollectionViewCell with an associated .xib file and setting outlets and such, we will just use a cell directly in the collection and use a tag to identify it:

The updateImageForCell: method has two important elements to consider:

  1. We need to clear the image as our first step. This will prevent the cells from showing images from other entries. We do this by setting the image to nil or, like in this case, to a fixed placeholder image.
  2. Then we will call our ImageManager asynchronous method downloadImageFromURL:completion:, and the trick here for avoiding setting the image in the wrong cell is checking that the indexPath for the cell is still the same indexPath that when the function was called. Only in this case we assign the image to the cell. We can easily validate this by calling tableView.indexPathForCell(cell), which will give us the current, updated indexPath for the cell right now, and compare it with the indexPath passed as argument to the function.

Now, our images are only shown in the right cells. Thanks to the image cache, we will update the images in the cells fast enough (once initially downloaded) to create a smooth scrolling with the right images. However, we can improve this experience even more with a simple trick.

Normally, the cells are only updated when the collection view reloads the data and redraws them. When downloading images for the first time, and scrolling throughout the collection view, the cells will not get refreshed until they are ordered to by the table or collection view. Thanks to our “right index check”, the images won’t appear unless they are on the right spot. We can add a manual trigger to refresh the images of the visible cells (only) when the scrolling has stopped.  This, together with our cache, will help us load always the right image and create the illusion of a smooth image flow. As a UITableView contains a UIScrollView, the UITableViewDelegate also acts as a UIScrollView delegate, and thus we can use scrollViewDidEndDecelerating: and scrollViewDidEndDragging:.

ezgif.com-optimizeThis is not really necessary, and the cells will load correctly without it, but in my tests, this little trick adds an extra bit of control on the image loading process. The result is a smooth scrolling with a nice lazy image loading. This same technique can be applied to a UICollectionView.

In my Github repository, you can download the full code of the project, with a sample App showing a UICollectionView and UITableView downloading a set of images from Flickr lazily.

 

 

 

 

 

 

4 Comments
  • Carlo

    February 8, 2016 at 5:34 am Reply

    Excellent tutorial: I always implemented this approach since my early days as iOS dev. In most cases many Pods can be replaced by custom code like this, that you control and optimize for specific needs.

  • Mark Horrocks

    June 14, 2017 at 2:22 pm Reply

    What is the purpose of withEntry? It’s not used in updateImageForCell.

    • Ignacio Nieto Carvajal

      June 15, 2017 at 8:04 am Reply

      Excuse me, I don’t know exactly what part of the code you are referring to. Consider that I wrote this code months ago, can you elaborate with a little more context?

  • […] If you want to know a little more about how I configured the collection view to display a specific number of rows, no matter the device, have a look at the Flawless collection and table views tutorial. […]

Post a Comment

Before you continue...

Hi there! I created the Digital Tips List, the newsletter for Swift and iOS developers, to share my knowledge with you.


It features exclusive weekly tutorials on Swift and iOS development, Swift code snippets and the Digital Tip of the week.


Besides, If you join the list you'll receive the eBook: "Your First iOS App", that will teach you how to build your first iOS App in less than an hour with no prior Swift or iOS knowledge.

I hate spam, and I promise I'll keep your email address safe.