UIKit | How to make modern infinite scroll view like the Unsplash app
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
- UIKit
- Combine
- URLCache
- UICollectionViewDiffableDataSource
- Unsplash API
- Without Storyboard
- Without any open sources(Kingfisher, SDWebImage, RxSwift, SnapKit, Then etc.)
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
structImageMetaData
. You can checkImageMetaData
’s full structure in here. - Key component in here is
dataTaskPublihser
. It wraps the well-known native APIURLSession
’sdataTask
as a Combine publisher.
You can use those publishers this way:
- Subscribe
fetchImageMetaData
where you need to get an arrays of meta data. Don’t forget to replaceClient-Id
with yours. - 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 methoddownloadImage
.
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 theUICollectionViewDelegateFlowLayout
which I will show you later in this post. - Also make sure you set the
isPrefetchingEnabled
true and adoptUICollectionViewDataSourcePrefetching
. 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 useUICollectionViewDiffableDataSource
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:
- Automatic data change animations
- Automatic data synchronization
- 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 withUICollectionViewDiffableDataSource
. - 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 asappendSections
orappendItems
, 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
UICollectionViewDelegateFlowLayout
UICollectionViewDelegateFlowLayout
allows you to determine the height of a cell dynamically.- In the
ImageMetaData
, you will remember that we have set a computed varianceimageRatio
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
inCacheManager
.
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’sfetchImageMetaData
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:
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.