Basics of Pull to Refresh for Swift Developers

Updated on September 21, 2016 – Swift 3.0

Implementing “pull to refresh” is a common need that arises when working with table views. There are typically two scenarios that folks find themselves in when attempting to implement this feature:

  1. They’re working with a UITableViewController
  2. They’re working with a non-UITableViewController, but their view incorporates a regular UITableView, either taking up the whole screen, or a smaller portion of it

This entry will explore both scenarios to help you get up and running quickly with implementing pull to refresh for your Swift iOS app.

Example scenario

For this guide, suppose that we have a list of movies that we’d like to display in a table view. Pulling to refresh will fetch more movies and update the table view to show the new ones.

If you’re the type that likes to simply dive into a working example, both implementations are available to download from GitHub:

Example projects
  • Example implemented with UITableViewController
  • <li>
      <i class="fab fa-github fa-lg"></i> Example implemented with regular view controller + UITableView <div>
        (<a href="https://github.com/andrewcbancroft/PullToRefreshExample_RegularViewController/tree/swift-2.3">Swift 2.3</a> | <a href="https://github.com/andrewcbancroft/PullToRefreshExample_RegularViewController/tree/master">Swift 3.0</a>)
      </div>
    </li>
    

Note: Code in the main article below is written in Swift 3.0, but code examples for Swift 2.3 are found in the example projects above.

Movies are represented by a simple struct:

1struct Movie {
2    let title: String
3    let genre: String
4}

The table view (regardless of whether we use a UITableViewController or a regular UIViewController) has the following setup…

Initial data source values:

1var movies = [
2    Movie(title: "Lion King", genre: "Animation"),
3    Movie(title: "Star Wars", genre: "Sci-fi")
4]

Setting up the table view’s data source protocol methods depends on whether you’re using a UITableViewController or a regular UIViewController with a table view as one of its content views, so we’ll cover those in the individual examples.

Implementing with UITableViewController

Finish example setup

When working with a UITableViewController, we simply override the data source method implementations. The following is how I’ve chosen to do it for this example:

 1override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
 2    return movies.count
 3}
 4
 5override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
 6    let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
 7    
 8    cell.textLabel?.text = movies[(indexPath as NSIndexPath).row].title
 9    cell.detailTextLabel?.text = movies[(indexPath as NSIndexPath).row].genre
10    
11    return cell
12}

Enable refreshing in Storyboard

When you’re working with a UITableViewController, the solution is fairly simple: First, Select the table view controller in your storyboard, open the attributes inspector, and enable refreshing:

Table View Controller - Enable Refreshing

A UITableViewController comes outfitted with a reference to a UIRefreshControl out of the box. You simply need to wire up a few things to initiate and complete the refresh when the user pulls down.

Override viewDidLoad()

In your override of viewDidLoad(), add a target to handle the refresh as follows:

1override func viewDidLoad() {
2    super.viewDidLoad()
3    // Do any additional setup after loading the view, typically from a nib.
4    
5    self.refreshControl?.addTarget(self, action: #selector(ViewController.handleRefresh(_:)), for: UIControlEvents.valueChanged)
6}

Here are a couple of things to observe about the code above:

  1. Swift’s new #selector feature helps with specifying which action will handle the refresh. Since I’ve specified ViewController.handleRefresh(_:) (note the underscore and the colon!) as the action argument, I need to define a function in this UITableViewController class with the same name. Additionally, the function should take one argument.
  2. We’d like this action to be called for the UIControlEvent called ValueChanged.

Implement handleRefresh function

The handleRefresh: function may look something like the following:

 1func handleRefresh(refreshControl: UIRefreshControl) {
 2    // Do some reloading of data and update the table view's data source
 3    // Fetch more objects from a web service, for example...
 4    
 5    // Simply adding an object to the data source for this example
 6    let newMovie = Movie(title: "Serenity", genre: "Sci-fi")
 7    movies.append(newMovie)
 8    
 9    movies.sort() { $0.title < $1.title }
10    
11    self.tableView.reloadData()
12    refreshControl.endRefreshing()
13}

That should complete the pull to refresh implementation when you’re working with a UITableViewController!

Implementing with regular view controller + UITableView

Finish example setup

When working with a regular UIViewController, there are a few extra steps involved in getting things set up:

  1. Create an IBOutlet from the storyboard to the view controller
  2. Wire up the table view’s data source and delegate from the storyboard
  3. Implement the required table view data source methods

Creating the IBOutlet is a matter of control+clicking and dragging from the table view in the Storyboard to the view controller code to create the outlet.

To wire up the table view’s data source and delegate in the Storyboard, control+click the table view and drag up to the yellow view controller icon:

Wire up table view's data source and delegate

The data source protocol method implementations may look something like this:

 1 Int {
 2    return movies.count
 3}
 4
 5func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
 6    let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
 7    
 8    cell.textLabel?.text = movies[(indexPath as NSIndexPath).row].title
 9    cell.detailTextLabel?.text = movies[(indexPath as NSIndexPath).row].genre
10    
11    return cell
12}

Set up UIRefreshControl

Whereas a UITableViewController comes pre-fit with a UIRefreshControl, a regular UIViewController does not. It’s simple enough to set one up though. Here is a snippet defining a lazily instantiated variable which creates and configures a UIRefreshControl:

1lazy var refreshControl: UIRefreshControl = {
2    let refreshControl = UIRefreshControl()
3    refreshControl.addTarget(self, action: #selector(ViewController.handleRefresh(_:)), for: UIControlEvents.valueChanged)
4    
5    return refreshControl
6}()

The most complicated thing about the code I just proposed is how the UIRefreshControl instance is assigned lazily by means of the closure expression denoted by = { // ...closure body with setup code... }() in the above snippet. Using this approach allows me to complete the setup all in one spot without the use of optionals. You may prefer doing this another way. The bottom line goal is to have a UIRefreshControl instance that we can add to the table view (coming up).

As for the body of the closure expression, we’re adding a target-action to the UIRefreshControl instance, just like we did when we were dealing with a UITableViewController.

As with the UITableViewController example, note:

  1. Since I’ve specified “handleRefresh:” (note the colon!) as the action argument, I need to define a function in this UITableViewController class with the same name. Additionally, the function should take one argument.
  2. We’d like this action to be called for the UIControlEvent called ValueChanged.

Override viewDidLoad

Assuming that there is an outlet to the table view in the Storyboard, the next step is to add the UIRefreshControl as a subview to the table view:

1override func viewDidLoad() {
2    super.viewDidLoad()
3    // Do any additional setup after loading the view, typically from a nib.
4    
5    self.tableView.addSubview(self.refreshControl)
6}

Implement handleRefresh function

The handleRefresh function is implemented exactly as it was when we were dealing with a UITableViewController:

 1func handleRefresh(refreshControl: UIRefreshControl) {
 2    // Do some reloading of data and update the table view's data source
 3    // Fetch more objects from a web service, for example...
 4    
 5    // Simply adding an object to the data source for this example
 6    let newMovie = Movie(title: "Serenity", genre: "Sci-fi")
 7    movies.append(newMovie)
 8    
 9    movies.sort() { $0.title < $1.title }
10    
11    self.tableView.reloadData()
12    refreshControl.endRefreshing()
13}

Wrapping up

Implementing “pull to refresh” is a common need that arises when working with table views. Here we’ve explored how to implement this feature using both a UITableViewController and with a regular view controller and a table view.

comments powered by Disqus