Mobile
Article
By Klevis Davidhi

UICollectionView DataSourcePrefetching Example and Explanation

By Klevis Davidhi

UICollectionView DataSourcePrefetching

Recently, Apple announced a new UICollectionViewDataSource protocol extension called UICollectionViewDataSourcePrefetching. This extension makes it possible to implement two new functions that deliver a completely smooth scrolling performance. This tutorial will show you how to use it.

You can find the source code on GitHub.

Setting up the Xcode project

Open Xcode and create a new project based on the Single View Application template. Before you do anything else, in order to improve the appearance of our application ,select the view and choose Editor -> Embed In -> Navigation Controller. Now drag a Collection View from the Object Library onto the view canvas and adjust its size .

Notice that into the Collection View is one reusable cell , so click under the Attributes Inspector and give this cell an Identifier, let’s call it foodCell. Drag an UIImageView onto it and adjust its size to fit the content.

Cell Identifier

Create a new file, of type Cocoa Touch Class , subclass of UICollectionViewCell, name it collectionViewCell and assign it to the cell.

collectionViewCell Class

Connect the UIImageView to the collectionViewCell.swift code and name it ‘foodImage‘. Also , connect the collectionView to ViewController.swift.

ImageView connection

Conforming to protocols and writing some lines

Now , it’s time to write some code in viewController.swift.
The first thing to do is to conform the class to UICollectionViewDelegate,UICollectionViewDataSource, and UICollectionViewDataSourcePrefetching protocols.

Declaring protocols

You should probably see some errors showing up in your sidebar, due to required methods not being implemented yet. We’ll cover that soon, but let’s finish a little setup first.

We’ll need a data source for this UICollectionView, so let’s just create an array called imageArray, which for illustrative purposes will store 30 images. Outside of any method, create the array like so:

var imageArray = [UIImage?](repeating: nil, count: 30)

and a variable to store the base url of a picture like so :

var baseUrl = URL(string: "https://placehold.it")!

Same as above create another array of type URLSessionDataTask :

var tasks = [URLSessionDataTask?](repeating: nil, count: 30)

The next step is to create two separated functions in order to generate images from dynamically generated urls. The first function requires a parameter which in our case will be the index of each cell and will return an url.

 func urlComponents(index: Int) -> URL {

        var baseUrlComponents = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true)
        baseUrlComponents?.path = "/\(screenSize.width)x\(screenSize.height * 0.3)"
        baseUrlComponents?.query = "text=food \(index)"
        return (baseUrlComponents?.url)!
    }

The second one is where the downloading process will be executed and it requires the indexPath of each cell as a parameter. The return type of this function will be URLSessionDataTask.

  func getTask(forIndex: IndexPath) -> URLSessionDataTask {
        let imgURL = urlComponents(index: forIndex.row)
        return URLSession.shared.dataTask(with: imgURL) { data, response, error in
            guard let data = data, error == nil else { return }

            DispatchQueue.main.async() {
                let image = UIImage(data: data)!
                self.imageArray[forIndex.row] = image
                self.collectionView.reloadItems(at: [forIndex])
            }
        }
    }

Make sure you have added these lines in viewDidLoad() method:

collectionView.dataSource = self
collectionView.delegate = self
collectionView.prefetchDataSource = self

Implementing required methods

Now , we must implement 3 required methods :

  • collectionView(_:numberOfItemsInSection:)
  • collectionView(_:cellForItemAt:)
  • collectionView(_:prefetchItemsAt:)

UICollectionView must know how many rows are going to be inside the section, so we’ll return the count of the elements in our array here:

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return imageArray.count
}

Before coming to the most important methods, there is only one more thing left to do. Let’s create a method where we can supervise the downloading process.
Once again the parameter required is the indexPath of the cell.

 func requestImage(forIndex: IndexPath) {
        var task: URLSessionDataTask

        if imageArray[forIndex.row] != nil {
            // Image is already loaded
            return
        }

        if tasks[forIndex.row] != nil
            && tasks[forIndex.row]!.state == URLSessionTask.State.running {
            // Wait for task to finish
            return
        }

        task = getTask(forIndex: forIndex)
        tasks[forIndex.row] = task
        task.resume()
    }
}

Finally, is time to build and return each cell. So in this method :

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let foodCell = collectionView.dequeueReusableCell(withReuseIdentifier: "foodCell", for: indexPath) as! collectionViewCell

        if let img = imageArray[indexPath.row] {
            foodCell.foodImage.image = img
        }
        else {
            requestImage(forIndex: indexPath)
        }

        return foodCell
    }

As we see the requestImage(forIndex: IndexPath) method helps us to observe if an image is already downloaded,is in process or is not downloaded at all.

--ADVERTISEMENT--

Prefetching

In order for your app to have “buttery smooth” performance, you must strive for app animation that performs at 60 frames per second. This means that a given frame of the user interface must be displayed in less than 16.67ms in order for the animation to appear “smooth.” Otherwise, when the frame rate drops lower than that, it’s apparent to the user in the form of a choppy animation.

Prefetching is a mechanism by which you are notified early before the collection view will need the cells to be displayed, thus providing an opportunity to prepare the cell’s data source in advance of actually creating the cell. Prefetching is provided by the UICollectionViewDataSourcePrefetching protocol and contains two instance functions:

public protocol UICollectionViewDataSourcePrefetching : NSObjectProtocol {

// indexPaths are ordered ascending by geometric distance from the collection view

@available(iOS 10.0, *)
public func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath])


// indexPaths that previously were considered as candidates for pre-fetching, but were not actually used; may be a subset of the previous call to -collectionView:prefetchItemsAtIndexPaths:

@available(iOS 10.0, *)
optional public func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath])
}

Implementing DataSource Prefetching

The first and required method is called when the Collection View is ready to start building out cells even before they are on the screen. An array of NSIndexPath objects is passed to the function and is used to prepare the data source. In our code we would write something like this:

    func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {

        for indexPath in indexPaths{

            requestImage(forIndex: indexPath)
        }

    }

Implementing Prefetch Cancellation

As mentioned above, the second method, collectionView(_:cancelPrefetchingForItemsAt:), is an optional feature that allows you to revert or clean up your data source when prefetching for an array of cells has been cancelled by the Collection View. This can happen when scrolling changes direction or becomes too rapid for prefetching to be implemented effectively. So let’s implement the last function in our code:


    func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
        for indexPath in indexPaths{
            if let task = tasks[indexPath.row] {
                if task.state != URLSessionTask.State.canceling {
                    task.cancel()
                }
            }
        }
    }

There may be times when you want to disable collection view prefetching. This can be done by setting the UICollectionView isPrefetchingEnabled property to false.

These new functions can also be used in UITableView as well just by implementing UITableViewDataSourcePrefetching protocol.

Conclusion

Using this protocol and its functions is a good choice . There’s no more need to worry about the performance of cells as they are ready to come to the screen before being seen and with a smooth scrolling performance, our application feels better.

Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the most important and interesting stories in tech. Straight to your inbox, daily.