Just Keep Scrolling... Scrolling, Scrolling!

We build professional apps for Android, iOS and mobile web.
Have a look

Creating A Table View That Just Keeps Loading | In Swift

In iOS development, UITableViews are a great way of showing long lists of information, as all of the complexities are handled automatically. The same can't always be said however when grabbing data from a server, especially when that data is paginated and retrieved asynchronously. On top of this, having a way of showing the user what's going on, without freezing up the UI, can lead to complex and messy code. Ideally, we would want a fluid UI, that loads more data when the user reaches the bottom of a list, and updates when the data returns, all without clogging the view controller. What we want, is to just keep scrolling!

Sample

Default Transition

Loading Footer

Having a loading indicator appear at the bottom of a list is an ideal way of showing the user more data is being retrieved. Not only is it contextually positioned, but it allows the user to keep interacting with other parts of the screen while the data is being retrieved. On many occasions, I have seen bulky third part libraries be introduced, or complex custom cells be added and manipulated in a UIViewController, all to achieve this very simple sounding task. But there is a better way, and it involves a brilliant UIView built right into UITableView called tableFooterView.

Using swift extensions, one can simply write a few simple methods, that allow any part of that application that uses a UITableView, to quickly and easily show and hide a bottom loading indicator. See the example below.

// MARK: Loading Footer

extension UITableView {

    func showLoadingFooter(){
        let loadingFooter = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
        loadingFooter.frame.size.height = 50
        loadingFooter.hidesWhenStopped = true
        loadingFooter.startAnimating()
        tableFooterView = loadingFooter
    }

    func hideLoadingFooter(){
        let tableContentSufficentlyTall = (contentSize.height > frame.size.height)
        let atBottomOfTable = (contentOffset.y >= contentSize.height - frame.size.height)
        if atBottomOfTable && tableContentSufficentlyTall {
            UIView.animateWithDuration(0.2, animations: {
                self.contentOffset.y = self.contentOffset.y - 50
            }, completion: { finished in
                self.tableFooterView = UIView()
            })
        } else {
            self.tableFooterView = UIView()
        }
    }

    func isLoadingFooterShowing() -> Bool {
        return tableFooterView is UIActivityIndicatorView
    }

}

These three methods are fairly self explanatory. When called on an instance of UITableView, they will show and hide the loading indicator immediately, while the third method can be used to gain information about the table view's current state. In practice, it's simple to know where to call the hideLoadingFooter, as this would simply be called in your callback after your server request for more data completes. The showLoadingFooter call however is slightly more complicated, as this would need to be called when the user scrolls to the bottom of a tableview. Let's see how this could be done.

func scrollViewDidScroll(scrollView: UIScrollView) {  
    let contentLarger = (scrollView.contentSize.height > scrollView.frame.size.height)
    let viewableHeight = contentLarger ? scrollView.frame.size.height : scrollView.contentSize.height
    let atBottom = (scrollView.contentOffset.y >= scrollView.contentSize.height - viewableHeight + 50)
    if atBottom && !tableView.isLoadingFooterShowing() {
        showLoadingFooter()
        // make api request
    }
}    

The above code uses the UIScrollViewDelegate method scrollViewDidScroll, as a place to check weather or not a user has just scrolled past the bottom of the table view's content. This method is fairly simple, but has a couple of gotchas.

1. While we want showLoadingFooter to be called when the user scrolls to the bottom of the list, we don't want it called if the list is simply so small that it doesn't extend beyond the bounds of the tableview.
2. We also don't want it being called again and again, as the scrollview moves pixel by pixel back to it's natural position, after being scrolled past it's bottom bounds.

To eliminate these issues we check to make sure the table view's content is sufficiently large, while also ensuring the footer is not currently visible. If these checks all pass, then we're all good to go.

Same Page Meme

Pagination

The second hurdle to overcome in this task is handling pagination, as this is something that can cause a lot of confusion, as well as messy, buggy code. I'm going to assume from the outset that you already have a service class setup for handling the actual server request, and what we're going to look at here is how this service get's called from the view controller. Instead of clogging the view controller with messy page management code, we're going to extract this out into it's own class that we will call APIController.

import UIKit

class APIController {

    private var isUpdating = false
    private var pages: [[Item]] = []

    func getNextPage(success success:SuccessBlock, failure: FailureBlock) {
        return loadItems(success: success, failure: failure)
    }

    func reloadData(success success:SuccessBlock, failure: FailureBlock) {
        self.pages = []
        return loadItems(success: success, failure: failure)
    }

    private func loadItems(success success:SuccessBlock, failure: FailureBlock) {

        if self.isUpdating {
            let items = self.pages.flatMap { $0 }
            success(items)
            return
        }

        self.isUpdating = true
        let nextPage = self.pages.count + 1

        // Calling API service
        APIService.sharedInstance.getItems("api.somewhere.com", page: nextPage, success: { items in
            self.pages.append(items)
            let items = self.pages.flatMap { $0 }
            self.isUpdating = false
            success(items)
        }, failure: { errorMessage in
            self.isUpdating = false
            failure(errorMessage)
        })
    }

}

The APIController class is very simple in its makeup, but brilliant in what it allows. By initialising and storing an instance of it in our view controller, all we need to deal with is calling any of two simple methods...
1.reloadData
2.getNextPage
What is brilliant about this is that both of these methods will then return, within their success block, the entire list of items to use as the data for the table view. No appending, removing or mutating of your data source in your view controller. It is all handled here, and returned as a beautiful, clean array. It just works.

Now in our view controller, all that is needed is two clear and simple methods, calling for new data from the APIController and controlling how the view handles the entirity of the new data.

func getNextPage() {  
    tableView.showLoadingFooter()
    serviceController.getNextPage(success: { items in
        self.items = items
        self.tableView.reloadData()
        self.tableView.hideLoadingFooter()
    }, failure: { errorMessage in
        print(errorMessage)
        self.tableView.hideLoadingFooter()
    })
}

func reloadData() {  
    serviceController.reloadData(success: { items in
        self.items = items
        self.tableView.reloadData()
        self.refreshControl.endRefreshing()
    }, failure: { errorMessage in
        print(errorMessage)
        self.refreshControl.endRefreshing()
    })
}

Wrap Up

Both dealing with pagination and creating responsive loading UI can be frustrating. Like many programming problems, it's not so much a question of "How can I code this?", but rather "How should I code this?". There are many different ways of solving these problems, all with their own benefits and disadvantages. But hopefully from reading this you gained some insight into one possible route.

If you want to check out a sample project showing how all this works together, click here.