Pull to Refresh Table View in Swift
May 04, 2015

Adding pull to refresh to a UITableView sounds like a lot more work than it actually is. The iOS UIRefreshControl makes it a quick & easy feature to implement. Today we’ll toss together a quick stock price list app and then add pull to refresh to get up to the second updates on demand. When we’re done it’ll look like this:

Pull to Refresh Table View
Pull to Refresh Table View

This tutorial has been updated for Swift 2.0, Xcode 7, and Alamofire 3.0.0.

If you just want the code, here it is: PullToUpdateDemo on GitHub or [zip][3].

API Call

First we need a tableview backed up some API data that changes often. Yahoo has a nice stock price API. They even provide a tool to test out your queries before building your app. To get stock prices from it we set up a query in YQL (similar to SQL). There are a ton of fields available but we’ll just grab the ask price and the high/low values for the past year for Yahoo, Google, and Apple:

select symbol, Ask, YearHigh, YearLow from yahoo.finance.quotes where symbol in ("AAPL", "GOOG", "YHOO")

If we plug that into the YQL tool, it spits out the super ugly URL to call to retrieve the results:

https://query.yahooapis.com/v1/public/yql?q=select%20symbol%2C%20Ask%2C%20YearHigh%2C%20YearLow%20from%20yahoo.finance.quotes%20where%20symbol%20in%20(%22AAPL%22%2C%20%22GOOG%22%2C%20%22YHOO%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys

And the results will look like:

{
 "query": {
  "count": 3,
  "created": "2015-04-29T17:22:39Z",
  "lang": "en-us",
  "results": {
   "quote": [
    {
     "symbol": "AAPL",
     "YearLow": "82.904",
     "YearHigh": "134.540",
     "Ask": "128.950"
    },
    {
     "symbol": "GOOG",
     "YearLow": "487.56",
     "YearHigh": "599.65",
     "Ask": "548.30"
    },
    {
     "symbol": "YHOO",
     "YearLow": "32.9300",
     "YearHigh": "52.6200",
     "Ask": "43.2300"
    }
   ]
  }
 }
}

Setting Up the Xcode Project

Most of this app is nearly identical to our previous project: Hooking Up a REST API to a UITableView in Swift. We’re just using a different API today.

So to get the project ready:

  • Create a new single view project in Xcode
  • In the storyboard, drag & drop a tableview into the main view for the view controller (or remove the UIViewController and replace it with a UITableViewController)
  • Add a prototype cell to the tableview with the subtitle style and set the reuse identifier to “Cell”
  • Add an ATS Exception for the API
  • Create a StockQuoteItem class in a new file with 4 string properties: symbol, yearHigh, yearLow, and ask
  • Add Alamofire & SwiftyJSON to the project
  • Use Alamofire & SwiftyJSON to extend Alamofire.Request and create a function to pull the data from the API and populate an array of StockQuoteItems (Follow along with Hooking Up a REST API to a UITableView in Swift but adjust the class & variable names as well as the endpoint and JSON parsing or check out the code on GitHub)
  • Fill out the ViewController so it’ll call the function to get an array of StockQuoteItems on launch and display them in the tableview cells. Put the API calling code in a function called something like loadStockQuoteItems() so we can call it again when we want to refresh the data

Here’s what my loadStockQuoteItems() looks like:

func loadStockQuoteItems() {
    StockQuoteItem.getFeedItems({ (items, error) in
      if error != nil {
        let alert = UIAlertController(title: "Error", message: "Could not load stock quotes \(error?.localizedDescription)", preferredStyle: UIAlertControllerStyle.Alert)
        alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.Default, handler: nil))
        self.presentViewController(alert, animated: true, completion: nil)
      }
      self.itemsArray = items      
      self.tableView?.reloadData()
    })
  }

At this point you should be able to run the app and see the stock info but the only way to refresh it is to restart the app.

Adding Pull to Refresh

Finally, the gritty details! In iOS UITableView and UIRefreshControl are designed for each other. So just add a refresh control to your view controller:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
  var refreshControl = UIRefreshControl()
  // ...

And add it to your tableview in viewDidLoad:

override func viewDidLoad() {
    super.viewDidLoad()

    // set up the refresh control
    self.refreshControl.attributedTitle = NSAttributedString(string: "Pull to refresh")
    self.refreshControl.addTarget(self, action: "refresh:", forControlEvents: UIControlEvents.ValueChanged)
    self.tableView?.addSubview(refreshControl)
    
    self.loadStockQuoteItems()
  }

Which will call the refresh: function so we need to implement it to reload our data:


func refresh(sender:AnyObject) {
  self.loadStockQuoteItems()
}

If you save & run now it’ll work but the refresh control won’t never go away. So we need to tell it to:

func loadStockQuoteItems() {
    StockQuoteItem.getFeedItems({ (items, error) in
      if error != nil {
        let alert = UIAlertController(title: "Error", message: "Could not load stock quotes \(error?.localizedDescription)", preferredStyle: UIAlertControllerStyle.Alert)
        alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.Default, handler: nil))
        self.presentViewController(alert, animated: true, completion: nil)
      }
      self.itemsArray = items

      // tell refresh control it can stop showing up now
      if self.refreshControl.refreshing
      {
        self.refreshControl.endRefreshing()
      }
      
      self.tableView?.reloadData()
    })
  }

Save & run to test. If you’re happy with the refresh control showing boring text all of the time then you’re done.

Showing the Last Refreshed Time

It’s more useful if the refresh control shows when you last refreshed as you pull it. So let’s add that. We’ll need to use a date formatter to display the last refresh time. NSDateFormatter is an expensive class to create (or to change the formatting) so we’ll create a single one and keep using it:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
  var refreshControl = UIRefreshControl()
  var dateFormatter = NSDateFormatter()
  // ...
override func viewDidLoad() {
    super.viewDidLoad()
    
    self.dateFormatter.dateStyle = NSDateFormatterStyle.ShortStyle
    self.dateFormatter.timeStyle = NSDateFormatterStyle.LongStyle
    
    self.refreshControl.addTarget(self, action: "refresh:", forControlEvents: UIControlEvents.ValueChanged)
    self.tableView?.addSubview(refreshControl)
    
    self.loadStockQuoteItems()
  }

Then we just need to set the text for the refresh control’s label each time we load new data:


func loadStockQuoteItems() {
  StockQuoteItem.getFeedItems({ (items, error) in
    if error != nil {
      let alert = UIAlertController(title: "Error", message: "Could not load stock quotes \(error?.localizedDescription)", preferredStyle: UIAlertControllerStyle.Alert)
      alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.Default, handler: nil))
      self.presentViewController(alert, animated: true, completion: nil)
    }
    self.itemsArray = items
    
    // update "last updated" title for refresh control
    let now = NSDate()
    let updateString = "Last Updated at " + self.dateFormatter.stringFromDate(now)
    self.refreshControl.attributedTitle = NSAttributedString(string: updateString)
    if self.refreshControl.refreshing
    {
      self.refreshControl.endRefreshing()
    }
    
    self.tableView?.reloadData()
  })
}

And That’s All

Save & run. You’ll see your refresh control display the last refresh time and it’ll update each time you refresh.

If you’d like more Swift tutorials like this one, sign up below to get them sent directly to your inbox. Feel free to come back and visit too, we’re pretty friendly around here but we’ll understand if it’s just easier for us to come to you :)

P.S. If you got tired of typing, here’s the final code: PullToUpdateDemo on GitHub or [zip][3].