UIKit | How to make modern infinite scroll view like the Unsplash app

Enebin
7 min readApr 18, 2022

--

Photo by Selwyn van Haaren on Unsplash

In this article I will show you how to implement infinite scroll in UIKit with Combine & DiffableDataSource.

Preview

Environment

  • iOS 13
  • Swift 5.x
  • Xcode 13.x

Developed using

It may seem odd to use Combine with UIKit. As you may know, however, SwiftUI is still incomplete in iOS 13 and seems too early to target iOS 13. Maybe this is why so many people still remain on UIKit.

We will use Combine instead of RxSwift. Combine has advantages that it is native library so does not need to be installed. It’s the reason that if you targeted iOS 13 for your app, you can consider about using Combine with UIKit.

Prerequisite

Delete storyboard

I’m not going to use storyboard in this project. It’s simply the matter of preference so if you want to use storyboard, do so.

Register your app with the API

You need to register your app in Unsplash to get the client ID used for all requests.

Now we are all ready to dive in.

Make publishers with Combine

Starting with the most important things, network requests.

Publishers help download image and image’s meta data from the network API and process those data asynchronously. We will create a manager class containing static methods to enhance usability.

  • To handle network response, I made Codable struct ImageMetaData. You can check ImageMetaData’s full structure in here.
  • Key component in here is dataTaskPublihser. It wraps the well-known native API URLSession’s dataTask as a Combine publisher.

You can use those publishers this way:

  1. Subscribe fetchImageMetaData where you need to get an arrays of meta data. Don’t forget to replace Client-Id with yours.
  2. You will get a url of an image from the response. To get an image we can use(ex. UIImage), we should request once again from the url. For this, you can subscribe method downloadImage.

I will show you how to use those later in this article.

Caching with URLCache

Caching large data, such as images, definitely improves application’s performance.

Mostly, the open sources like Kingfisher and SDWebImage does all these things for us so usually we don’t have to care about this irritating jobs.However we are going to do it ourselves in here to understand these things more deeply.

You can skip this part and replace with any open sources if you want.

CacheManager

It’s quite simple. Make a cache storage and check if a request has stored in the cache storage. If it’s not stored, add logic to save the data.

In ImageFetchManager

You can use these methods in ImageFetchManager.downloadImage like:

Make UI with UIKit

UICollectionView

There’s no doubt that you should use UICollectionView or UITableView when displaying repeating cells. You can use either one depending on your preference.

You can check the full code for view controller and view model.

ViewModel

  • The view model starts fetching data the moment the instance is created. Fetched image data should be connected data source later.
  • It is also responsible for updating data when requested. Update requests are triggered by the view controller based on scroll position. It’s commonly referred to as lazy loading.

UICollectionView

  • I applied UICollectionViewFlowLayout to decide each cell’s height dynamically. It will be implemented through the UICollectionViewDelegateFlowLayout which I will show you later in this post.
  • Also make sure you set the isPrefetchingEnabled true and adopt UICollectionViewDataSourcePrefetching. It helps avoid choppy scroll when scrolling down the screen quickly.
  • As you can see, I didn't adopt UICollectionViewDataSource delegate in this project because instead of that I will use UICollectionViewDiffableDataSource as a data source.
  • Please don’t forget adopting UICollectionViewDelegate even though you are not using it. If this line’s omitted, cells size is set unexpectedly.

UICollectionViewCell

Each cell is designed to show an image with author’s name at the bottom left side.

You can check full codes in here.

ViewModel

  • A bit tricky thing in here is when a cell is created, we have to make another request to download the image data, unlike the author name.
  • There are many ways to pass data to child view controller or view models but I chose to pass the url and handle this information from child view independently. It has the advantage that we can avoid requesting too many data at once.
  • This method can have some disadvantages in the aspect of loading speed compared to the method forwarding data directly. It’s because a cell will fetch image at the time it’s shown at the screen.
  • It can be avoided to some extent using pre-caching but choose the method according to the part you focus on.

View

  • Cell’s image component(UIImageView) is connected to view model to be updated right after the image’s downloaded.
  • Don’t forget to inject its view model from outside.

Set datasource with UICollectionViewDiffableDataSource

UICollectionViewDiffableDataSource is newly introduced in iOS 13. It has some advantages compared to the original UICollectionViewDatasource like:

  1. Automatic data change animations
  2. Automatic data synchronization
  3. Reduced code

You can get more detail from the official document or tutorial.

  • What I did in here is connecting view model’s ImageMetaData array with UICollectionViewDiffableDataSource.
  • Before making the data source, set Section enum. It’s necessary to make data source.
  • To update the data source, use NSDiffableDataSourceSnapshot. It literally snaps current state of data source and offers the methods, such as appendSections or appendItems, which help us change the values.
  • If you finished any update logic and want to migrate new data, just apply the snapshot.

Set flow layout & prefetching

Cell’s height is decided dynamically by its image’s aspect ratio

UICollectionViewDelegateFlowLayout

  • UICollectionViewDelegateFlowLayout allows you to determine the height of a cell dynamically.
  • In the ImageMetaData, you will remember that we have set a computed variance imageRatio calculating aspect ratio of the image.
  • Using the imageRatio, it’s possible to calculate cell’s proper height when width of cell is same with the screen’s.

UICollectionViewDataSourcePrefetching

  • To avoid choppy scrolling, prefetching data before reaching the end of page can be a solution. In this project, another page of ImageMetaData can be prefetched when it arrives at the 3rd cell from the bottom. The location of cell that triggers prefetch can be adjusted at will.
  • We can also improve load speed by pre-caching the image of the next cell in advance. It’s done by the method cacheImage in CacheManager.

Add drag refresh

It can be implemented by adding UIRefreshControl. However, if you merely added it to your collection view, you will be facing the animation which never stops even after refresh has already been done.

To stop the animation at the right time, we should keep observing a progress of network request. Fortunately, it becomes really easy with Combine.

  • One of the best practices to handle progress of network request is using enum. You can define all possible cases and update shared variable using the enum.
  • Process, viewModel.process will be the case in here. It determines when the animation should start or stop.
  • Combine’s method sink offers us a very convenient feature, receiveCompletion. Using this, we can detect when the request completed or failed with certain error. You can check this in the first code block’s fetchImageMetaData

Now it’s done

It’s been a long way. There will be more to improve this project such as full screen page view like the official Unsplash application. It will probably look like the one below:

If I have time, I might write an additional post about this later.

Source codes

Thanks for reading it 😀

Many parts of the codes are not described in this post. I highly recommend you to take a look at the source codes below.

--

--

Enebin
Enebin

Written by Enebin

Making your dreams a reality |  Mainly Interested in iOS | https://github.com/enebin

No responses yet