iOS CollectionView Download & Display Images

One of the challenges every iOS developer faces on regular basis is loading data from the server, while keeping the UI responsive.
PhotoDisplayer
In this article we’re going to build a simple CollectionView Controller that fetches images from the server asynchronously while allowing the user to interact with the UI by either scrolling up, down, or selecting an image to view it in full screen. This tutorial assumes that you understand the basics of iOS development, and have worked with either UICollectionViewController or UITableViewController before but no prior networking or concurrency knowledge is required.

In this tutorial we will learn how to:

  • Offload the image fetching from the Main UI thread to make the User Inferface responsive
  • Avoid a common scenario when the wrong images are displayed in the cells after a scroll due to reusable cells
  • Cache the images so that we don’t re-download them next time the cells become visible
  • Only download images for visible cells. When the cell becomes invisible, stop the download to save bandwidth for the newly visible cells
  • If the image download was stoped due to the cell becoming invisible, save the download progress so that it can be resumed next time it becomes visible again


Download the starter project. This is a very naive and inefficient implementation that we’re going to use as a starting point and make it more robust. This tutorial will focus on networking and concurrency so while you don’t need to be an iOS expert, you should have a basic understanding of how the UICollectionView and/or UITableViews works, in order to follow this tutorial. All the UI code is already implemented so we will concentrate on the images downloading aspect only. Having said that, feel free to familiarise yourself with the code and see how things work. For the actual images, we will be using a service called https://picsum.photos to download the images to our app but that’s has already been (inefficiently) implemented.

Make sure that you can run the sample project in Xcode and if everything went well you should see the main screen with image thumbnails collection being loaded. Try scrolling the collection down and tapping the thumbnails to view the images full screen. While the app does it job, you probably won’t disagree with me that it feels very ‘sluggish’ and it’s probably not a great user experience after all.

Let’s have a look at the method that constructs the cells for the collection view to see if we can spot why the scrolling is so slow. Open up the ImageFetcherController+DataSource.swift file and navigate to cellForItemAt method:

What is happening in this method?

  • A cell is being dequeued at line 2 from the reusable cells collection
  • We than construct a URL based on the position of the cell which will be used to download the image
  • At line 6 we do the actual data downloading for the image
  • Next, at line 8 we check that the data has been downloaded successfully and construct the image
  • We then set the cell with the image that we have just downloaded
  • Finally, we return the cell for it to be rendered by the collection view

The biggest issue with the above code is that the image downloading is done on the main UI thread at the point of constructing the cells. When the page is initially loaded, collection view will ask for all the cells that are visible on the screen to be rendered. The problem is that until ALL of the images for those cells are downloaded, the UI will be frozen. As the user scrolls, the collection view will be asking for new images to be rendered and until the all of those images are downloaded, the UI will appear to be frozen or clunky.

A good idea would be instead of downloading an image before we return a cell (and slowing down the whole UI as discussed above), we return the cell immediately without the image, perhaps with a loading spinner indicating that something is loading, and when the image is downloaded, we simply set the content of that cell with the downloaded image avoiding locking the UI all together.

This can easily be done by moving the downloading and fetching of the image into a new thread. Bellow is a modified cellForItemAt method with threading support added:

As you can see in the highlighted code above, starting at line 6, we have set the cell image to nil which is necessary to clear any previous images on the cell (remember that those cells are reused) and will also make the loader icon spin, indicating that image is being fetched. At line 8, we add the image downloading code to a global asynchronous queue, which basically means that any code within its opening and closing brackets will be run on a separate thread, without holding the main UI thread. On line 9 we download the data just as before and do the necessary checks to construct the image at line 11 as we have done before. Next, we could probably set the cell with newly downloaded image directly, but remember that we are essentially on a non UI thread at the moment and interactive with UI directly can cause all sort of issues. Instead, before setting the image, we need to get back onto the main UI thread. We no longer worry about blocking the UI thread as we have already downloaded the image and all we’re going to do is just set it onto the cell. On line 12 we use DispatchQueue.main.async wrap all the UI interactions. You may have noticed on line 15 that there is a new code added that checks for selected image and sets the imageView it if there is one. This is necessary if the user has navigated to the full screen image view page before the image was downloaded, and this code will update that view.

Run the app and play with the UI by scrolling up and down. Notice how fast and responsive to collection view has become. It immediately displays the thumbnails before they are downloaded and uses a spinner to indicate that download is in progress.

While we have made an improvement, we have also introduced a couple of issues. If you scroll down the collection view enough for the initial thumbnails to disappear and than, before all the new images are loaded, scroll up quickly to the top, you will notice that the previously downloaded cells now appear to be downloading again. Even worst, when the images do appear, they are initially incorrect ones only to be later replaced by, hopefully correct ones. Before we fix those two problems, let’s try to understand why when you scroll fast you get this effect of cells flickering with multiple images.

The cells flickering issue is a fairly common problem that happens to many new iOS developers (even the official Instagram iOS app has this problem last time I checked!) so I think it is worth spending a bit of time understanding why it happens before we implement a fix. Essentially this bug has to do with the cells being reused by the collection view. Each time a reusable cell appears to the user, it triggers an image download. If you where to scroll slowly, allowing the previous download to fully complete for the cell before that cell is reused again for a different image, then there would be no flickering as only a single download would of been in progress for each cell at any given time. The problem starts when you scroll fast enough and the cells do not not have enough time to complete the first download before the second (or even a third, if you scroll fast enough) has began. As you can imagine, when the first download is finished, than that image will be displayed on the cell (even if that cell is now responsible for a completely different image). At some point the second (third and so forth) download is also finished which causes a new image to be set onto the cell hence we see the flickering.

If you still think you don’t fully understand the flickering problem, have a look at the three figures above. This is a very simplified example of either a collection view or a table view (the principles are exactly the same) that has 15 images to display in total and only 5 cells are visible at any given time on the screen. For our example, let’s assume that the collection (or table) view will only produce 5 reusable cells and as the user scrolls, the cells will get reused (in the real collection or table view there would probably be 6-7 reusable cells to allow for smooth transition when the user scrolls, but the concept is the same) to display different images.

Figure 1 shows the initial state of the table without any scroll. There are 5 cells visible (numbered from 1 to 5) that display 5 images (also numbered 1-5) and no active downloads are in progress.

Scenario 1: Slow scroll:
In this scenario we will explore what is going to happen when we scroll slowly (as slow as it is necessary for the image download to complete for the cell before it get reused again). As we start to scroll down, the Image 1 becomes invisible as it’s scrolled off the screen and the Image 6 now appears from the bottom. Behind the scenes, the collection view has figured out that the Image 1 is no longer visible and has added it to it’s own queue of available cells for later re-use. Before the Image 6 becomes visible, the collection view needs a cell to render the new image. The collection view is able to get a cell by taking the first one from the available cels queue which happens to contain the Cell 1 which was previously used to display the Image 1. Before the Cell 1 for Image 6 is displayed however, the cellForItemAt method is run, where the previous image (Image 1) is cleared. Next a download begins asynchronously and the cell with a loading spinner is returned, ready for display. At some point, when the Image 6 download is complete, ta completion handler is triggered in cellForItemAt which will assign the downloaded image to the cell.

Scenario 2: Fast scroll:
In this scenario we will start from the initial screen as in the first scenario. This time we will scroll fast to the bottom of the collection view. As we scroll the Image 1 off the screen, just as in the Scenario 1, the Cell 1 is recycled and than given to the Image 6 as it appears from the bottom. Again, as in the previous scenario, the previous image is cleared from the cell and a new download is started before returning the cell to the screen. This time however, we continue to scroll, and before the Image 6 has been downloaded, it is scroller off the screen, causing the Cell 1 to be recycled again, and given to Image 11 as it appears from the bottom. At this point, the cellForItemAt is called again, which initiated a new download for the Image 11, yet the Image 6 has yet to be downloaded. Hopefully you can now foresee what is going to happen. At some point, the Image 6 will arrive and the completion handler will set it onto the Cell 1, which is no longer related to this image as it expects the Image 11. When the Image 11 arrives, than it will replace the image once again, causing the annoying flickering effect. At least we where lucky to end up with the correct image in this scenario! What if the Image 6 was 3MB in size for example, and the Image 11 only 1MB? The most likely outcome of this would be that the second request of Image 11 would be finished downloading before Image 6 causing the Cell 1 to end up displaying a totally wrong image. As you can see, this is a very serious problem and we need to fix it.

Let’s begin our refactoring journey by creating an ImageTask class which will be responsible for downloading the images. Create a file named ImageTask.swift with the following content:

Before we dig into the code, create one more file named ImageTaskDownloadedDelegate.swift with the following code:

Looking at the ImageTask.swift we can see the following parameters:

  • Position: this is the image position within the collection view
  • A URL object that points to the image location
  • A shared session url object that will be used to create individual tasks for downloading the images. We could of used individual sessions for each download, but it’s considered a good practice to keep the related tasks within the same session
  • A delegate, which will be used to notify the collection view when an image is downloaded

We have also created the ImageTaskDownloadedDelegate protocol (that will be passed to ImageTask as a delegate) which contains a single method definition of imageDownloaded. If you’re new to protocols/delegates but familiar with C#/Java interfaces than it should hopefully be easy to understand what they’re used for. The protocol is like a C#/Java interface that contains only method signatures that will be implemented by the class that implements this interface/protocol. We will implement this protocol in ImageFetcherController later and pass the ImageFetcherController to ImageTask as a delegate, so that ImageTask can later call a imageDownloaded method of the ImageFetcherController to notify it that a download has been completed. With that out of the way, let’s complete the ImageTask by finishing the resume method. This method will handle both the initial download start and the resume when the download was paused:

This method first checks if the resumeData is available, which is created when a download is paused and used to start the download from the point it had originally stopped. If the resumeData is not available this means that we need to start a fresh download. Either way, when the download is completed the downloadTaskCompletionHandler will be called. Finally, creating the task is not enough, we also need to call a resume() on it (resume is called regardless if it’s a new download or a resumed one).

The pause method is very simple. All we need to do is to call the cancel method on the task, which, as you may have guessed, will cancel the task, but will also save any progress made to resumeData that we can use later use to resume the download.

This completion handled is called when a download task has completed downloading the image data. At the very top we check for errors and than continue as follows:

  • We unwrap the URL returned by the download task. This url is not the location where the file was downloaded from but rather the location of the newly downloaded file on the disk
  • We load the data from the local file into memory which we have accessed by the URL provided in the previous step
  • Finally we create the UIImage instance from data

Once the download and conversion of the data to UIImage is complete, we assign the new image to the ImageTask public property (which will later be accessed by the collection view) and notify our delegate (which is also the collection view) that the download has been finished. The last two steps are performed on the main UI thread because the invocation of the downloadTaskCompletionHandler was done asynchronously on another thread so we need to get back to the correct thread before we do any UI interactions.

Now that we have implemented the ImageTask all left to do is to hook it up in ImageFetcherController. We will start by conforming ImageFetcherController to ImageTaskDownloadedDelegate protocol:

Once the ImageFetcherController is conforming to ImageTaskDownloadedDelegate we need to create the imageDownloaded(position: Int) method which is defined by the protocol:

Next we need to create the a dictionary that will keep a track of all the ImageTasks and some way of initially populating it. Add the bellow imagesTasks definition at the top of ImageFetcherController:

Find the finishedFetchingImagesInfo and modify it to include a new call to setupImageTasks:

Bellow, declare the actual setupImageTasks method:

Here we first create a shared URL Session and then create ImageTasks for every single image in our collection view and assign those tasks to imageTasks dictionary with their corresponding position used as a key.

Now we need to override two UICollectionView helper methods that will notify us when a cell is about to appear and disappear. We need this to be able to trigger the start/resume and the pause of the image download for the correct cells. Navigate to ImageFetcherController+DataSource.swift and add the bellow two methods:

The above two methods are self explanatory, the willDisplay cell method will cause a cell to resume a download and the didEndDisplaying cell will pause it.

If we look at the cellForItemAt method in the same file however, we still see that there is an old image fetching implementation left. We need to simply delete it and retrieve the correct image for the cell from the imageTasks dictionary we declared and populated earlier:

Finally, there is just one more thing left to do before our app is fully functional. Go back to ImageFetcherController and change the imageDownloaded method that we added earlier:

This method is called when an image has finished downloading. At line 2 we simply refresh the corresponding cell of the collection view and on line 4 we check if a user has selected an image for preview and update that screen as well.

At this point we have finished with the refactoring so it’s time to run the app. If everything has gone well, you should now see a much more robust application that is able to handle multiple downloads at once without any of the limitations we faced in our original design.

Final link to download.

Please let me in the comments if you have any issues with this tutorial or any suggestions.

19 Replies to “iOS CollectionView Download & Display Images”

  1. Dude, there is some serious issue with your starter and final project. When I download the projects, it does not have ‘.xcodeproj’ file. How will I compile the project then? Please make sure if you have uploaded the correct files or not. Otherwise this tutorial is a waste without those proper starter and final project.

  2. Very helpful blog. I have been searching for this a long time. But I have one problem: Do you know why there could be a laggy moment when a new cell appears where the image has not been loaded yet?

    1. Hi Anton, my guess is that you’re trying to download the images on the main (UI) thread which basically blocks the UI until the network request has been processed hence the UI feels laggy. The correct approach is to download & process the images on a background thread and only when they’re ready switch to the UI thread to display them.

      In the example code for this tutorial, I start with a naive approach by downloading the images on the UI thread (and the lagginess of the UI is very evident) and then improve the code in order to fix this, among with other, issues.

      1. Hi Alex! I tried to run the code on the global thread until I found out that many image downloads get cancelled before they finish successfully. The downloads seem to be cancelled randomly and I couldn’t find the reason for that.

  3. Hi guys, apologies for those of you that tried to use the example code from github and discovered the pbxproj file was missing. This was caused by my .gitignore rules, which I have now fixed so everything should be working as expected. If you still have any issue, please let me know.

  4. Hi Alex, thank you for the tutorial, i fixed my problem and have now deep understanding how collection view need to be implementend, i have a question in your project, in the downloadTaskCompletionHandler method, you check for a cache downoaded and then load it into the uiimage, there is any way to create a persistent cache of this and check it before download?, every load of the collection start downloading all of the images. can you give me some advice in this way? thank you

    1. Hi Victor, yes, caching images to disk is possible and also something I recommend doing for a production level app. The simplest way I think this can be done:

      1) in downloadTaskCompletionHandler() of ImageHandler, use the FileManager API to save the file somewhere, preferably in the user documents. You can use the URL after the domain name as a file name.

      2) In resume() method of the same class, before you create a session task, query the user directory (using the FileManager API) to see if there is a file with the same name as the URL (excluding the domain name). If such a file found, you can skip the downloading of the file, else proceed with the session task as normal.

      If you get stuck please let me know. I could potentially create a new branch with the file caching logic.

  5. Excellent Job Alex. I have tested out your final project and if scrolling fast then slow back to tiles past the tile remains in loading status but that can be over come by scrolling past and back again. This however is not the problem I seek to overcome. I have added caching to this solution and taken it to another level where the collection view (horizontal) is now embedded in a table view, something like a netflix feel. This little problem has now become a major problem. even for a small data set.

    Any Ideas?

    Thank you in advance,
    Reinaldo

      1. Hey Reinaldo, glad you was able to fix it in the end. Sorry did not get back to you sooner, was a bit busy last couple of days working on a new blog post/tutorial. If you enjoyed this one then please stay tuned 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *