Adding an API-Backed UIPageViewController in Swift
August 19, 2015

Table views are the workhorse of iOS apps but sometimes being able to swipe through the data makes more sense. It’s particularly useful when there’s too much data to easily show in a tableview and the user just needs to pick something that’s good enough. If I were writing a Netflix app for myself I’d be tempted to use a page view: I’m usually just looking for something interesting enough to watch and I want to read the details about the movies instead of just seeing the titles and cover images.

Whatever your motivation for using a UIPageViewController, today we’re going to work through how to use one to display data from a REST API. We’ll be working with the Yahoo Finance API to get stock quotes again, like in Pull to Refresh Table View in Swift and NSNotifications in Swift. Here’s what our demo app will look like when it’s done:

This tutorial was written using Swift 1.2, Xcode 6.4, Alamofire v1.3.1 and SwiftyJSON 2.2.1.

To see how to update this app to Swift 2.0, iOS 9, and Alamofire 3.0.0 see Updating to Swift 2.0 and iOS 9.

How Page View Controllers Work

In table view controllers or collection view controllers each element is a cell. A cell is a special type of view. In page view controllers the elements are view controllers, not just lightweight views. That allows us to more easily split up code to keep it easier to understand. It also makes it easy to use totally different views for each page but that’s not a very common requirement. Nice to know you can do it if you want to though.

Setting Up a UIPageViewController

You can use Xcode to generate a project that already includes a Page View Controller but if we’ll understand it better if we set it up ourselves.

If you’d rather not type along, grab the completed code from this tutorial on GitHub.

So open Xcode and create a new Single View application using Swift. We’ll need to swap out the existing UIViewController for a UIPageViewController. There are a few steps so I’ve added a video in case you’d rather follow along that way:

Here’s what we need to do:

  • Open Main.storyboard
  • Make sure the View Controller is selected (the yellow square-within-a-circle is the icon for view controller)
  • Notice in the right panel that the “Is Initial View Controller” option is selected. That tells Xcode which view controller to load when the app launches. We’ll need to set that option on our UIPageViewController
  • Delete the View Controller
  • In the bottom of the right panel (the Object Library), find the Page View Controller
  • Drag a UIPageViewController onto the storyboard
  • Turn on that “Is Initial View Controller” option for the new UIPageViewController

Ok, so now we’ve got a UIPageViewController set up as the initial view controller in our app. We’ll need some code to back that up:

  • Delete the ViewController.Swift file
  • Create a new Swift file. Name it something like GrokPageViewController.swift
  • Open up the new file
  • Declare a new UIPageViewController class in the new file:

class GrokPageViewController: UIPageViewController {
    ...
}

Hop back to the storyboard and change the class of the page view controller to your new custom class:

Add Today Widget Target
Set custom class in Storyboard for UIPageViewController

Since our app is pretty simple we’ll also use this class as the data source for our page view controller:


class GrokPageViewController: UIPageViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    self.dataSource = self
  }
}

Checking the docs about UIPageViewControllerDataSource we can see that there are 2 required methods:


pageViewController(_:viewControllerBeforeViewController:)
pageViewController(_:viewControllerAfterViewController:)

So we’ll need to implement those two methods. For convenience, we’ll also add a viewControllerAtIndex method to include the common code that both of those methods will require:


import UIKit

class GrokPageViewController: UIPageViewController, UIPageViewControllerDataSource {
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    self.dataSource = self
  }
  
  // MARK: UIPageViewControllerDataSource & UIPageViewControllerDelegate
  func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
    // TODO: implement
    return nil
  }
  
  func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
    // TODO: implement
    return nil
  }

  func viewControllerAtIndex(index: Int) -> UIViewController? {
    // TODO: implement
    return nil
  }
}

Ok, that’s a nice little framework but we’re missing a few things:

  • Fetching the stock quote data from the API
  • A view controller that knows how to display a stock quote
  • Something to hold the stock quotes so we can get them for each page

Let’s get the API calls nailed down first, then figure out how to fit the data into the UI.

Our Stock Item Class

Yahoo has a neat set of APIs including one for stock quotes. They let us pass in a query similar to SQL. We’ll stick to just a few simple fields: the stock symbol, the ask price and the high/low values for the past year. We’ll use the Yahoo, Google, and Apple stocks but you can sub in any stock symbols that you want. So the query that we’ll use will be:

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

And the result will look like:


{
    "query": {
        "count": 3,
        "created": "2015-04-29T16:21:42Z",
        "lang": "en-us",
        "results": {
            "quote": [
                {
                    "symbol": "AAPL"
                    "YearLow": "82.904",
                    "YearHigh": "134.540",
                    "Ask": "129.680"
                },
                ...
            ]
        }
    }
}

Like the previous tutorials using this API, we’ll need a class to hold our stock quotes. So create a new StockQuoteItem file and fill it in with a few properties:


import Foundation

class StockQuoteItem {
  let symbol: String?
  let ask: String?
  let yearHigh: String?
  let yearLow: String?
}

The endpoint that we need to hit that contains the query is kind of ugly. To generate it, we’ll take the query, URL encode it so we’re not sending weird symbols, then stick that into the API call to take a query. So it’ll be something like:


https://query.yahooapis.com/v1/public/yql?q=*encoded query*

So:


class StockQuoteItem {
  let symbol: String?
  let ask: String?
  let yearHigh: String?
  let yearLow: String?

  class func endpointForFeed(symbols: [String]) -> String {
    let symbolsString:String = "\", \"".join(symbols)
    let query = "select * from yahoo.finance.quotes where symbol in (\"\(symbolsString) \")&format=json&env=http://datatables.org/alltables.env"
    let encodedQuery = query.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
    
    let endpoint = "https://query.yahooapis.com/v1/public/yql?q=" + encodedQuery!
    return endpoint
  }
}

We’re going to be getting those API results in JSON so it’s fair to assume that we’ll want to create a StockQuoteItem from a hunk JSON. We’ll be able to parse the JSON to extract just the stock quotes so we can pull the data from each of those JSON elements:


class StockQuoteItem {
  let symbol: String?
  let ask: String?
  let yearHigh: String?
  let yearLow: String?

  required init?(json: SwiftyJSON.JSON) {
    self.symbol = json["symbol"].string
    self.ask = json["Ask"].string
    self.yearHigh = json["YearHigh"].string
    self.yearLow = json["YearLow"].string
  }

  ...
}

API Call

Ok, so how about that API call? I mean, we’ve got an endpoint, let’s use it! This is our first tutorial getting into the beautiful new serialization in Alamofire 1.3, so let’s see what that turns out.

From previous work with Alamofire we know that we’ll end up doing something like this:


class StockQuoteItem {
  ...
  class func getFeedItems(symbols: [String], completionHandler: ([StockQuoteItem]?, NSError?) -> Void) {
    Alamofire.request(.GET, self.endpointForFeed(symbols))
      .responseArrayAtPath(["query", "results", "quote"], completionHandler:{ (request, response, stocks: [StockQuoteItem]?, error) in
        completionHandler(stocks, error)
    })
  }
}

That’s a lovely little piece of code, other than the fact that responseArrayAtPath doesn’t exist. If it did, I suppose it would extract an array from the response to our URL request. Let’s just live with the fact that it’s imaginary for a minute and see what this code would do if that function existed:


class func getFeedItems(symbols: [String], completionHandler: ([StockQuoteItem]?, NSError?) -> Void) {

We’re declaring a getFeedItems function. It takes in an array of stock symbols as a [String] and a completion handler block with an array of StockQuoteItems and an error (both optional, of course).


Alamofire.request(.GET, self.endpointForFeed(symbols))

We’re using Alamofire to make a GET request to the endpoint that our handy dandy little function puts together from the array of stock symbols.


.responseArrayAtPath(["query", "results", "quote"], completionHandler:{ (request, response, stocks: [StockQuoteItem]?, error) in
        completionHandler(stocks, error)
    })

We would call our hypothetical responseArrayAtPath with the path to the array that we want to parse out of the JSON: ["query", "results", "quote"]. We’d set up a completion handler to get back the response & request along with an array of StockQuoteItems and any errors that occured. Then we’d pass the StockQuoteItems and error back to the calling function.

Sounds like responseArrayAtPath would work pretty nicely. Might even be handy in other projects. How about we implement it?

We’ll need Alamofire and SwiftyJSON (your project is probably already complaining about not having SwiftyJSON). So close Xcode and add those libraries using CocoaPods.

Not sure how to add those pods? Head over to A Brief Intro to CocoaPods for detailed intructions on adding libraries using CocoaPods.

  • Open up the .xcworkspace file that CocoaPods generated
  • Create a new file called AlamofireRequest_ResponseJSON.swift since we’ll be extending Alamofire.Request
  • Import Alamofire & SwiftyJSON
  • Declare our extension to Alamofire.Request to match the signature that we used for .responseArrayAtPath:

import Foundation
import Alamofire
import SwiftyJSON

extension Alamofire.Request {
  public func responseArrayAtPath(pathToArray: [String]?, completionHandler: (NSURLRequest, NSHTTPURLResponse?, [T]?, NSError?) -> Void) -> Self {
    ...
  }
}

<T: ResponseJSONObjectSerializable> and [T] might seem kind of odd. Well, we said this code might be handy in other projects and those projects won’t be using StockQuoteItem objects. So we’re making this function generic: it can work with different types of objects. <T> is how we specify that a function is generic. The T is just a placeholder for the class that’ll be used, here it’ll be StockQuoteItem. So whenever you see T you can just read “StockQuoteItem or whatever class we’re using this function with”.

<T: ResponseJSONObjectSerializable> is a little more complicated. We’re saying responseArrayAtPath will use some class but also that class will implement a protocol called ResponseJSONObjectSerializable. That sounds super fancy but like a lot of fancy stuff if you read each bit carefully it turns out it’s not that complicated at all. So ResponseJSONObjectSerializable: we’ve got serializable (i.e., can be converted from or into a series of bytes) from a response (to an Alamofire.Request), Response, JSON, and Object. So we’re gonna be starting with the bytes of data in a response and getting an Object via JSON. So really, all we’re saying is that we want to be able to create those objects from JSON that’s coming in a response to a web request. Oh, we did that already for StockQuoteItem, didn’t we?


class StockQuoteItem {
  let symbol: String?
  let ask: String?
  let yearHigh: String?
  let yearLow: String?

  required init?(json: SwiftyJSON.JSON) {
    self.symbol = json["symbol"].string
    self.ask = json["Ask"].string
    self.yearHigh = json["YearHigh"].string
    self.yearLow = json["YearLow"].string
  }

  ...
}

Let’s just go ahead and define that protocol:


public protocol ResponseJSONObjectSerializable {
  init?(json: SwiftyJSON.JSON)
}

extension Alamofire.Request {
  ...
}

And we can declare that StockQuoteItem already implements it:


class StockQuoteItem: ResponseJSONObjectSerializable {
  ...
}

Ok, so now we need to sort out what happens between getting a response and turning it into StockQuoteItems using chunks of JSON:


extension Alamofire.Request {
  public func responseArrayAtPath(pathToArray: [String]?, completionHandler: (NSURLRequest, NSHTTPURLResponse?, [T]?, NSError?) -> Void) -> Self {
    let responseSerializer = GenericResponseSerializer<[T]> { request, response, data in
      if let responseData = data
      {
        // TODO: stuff
      }
      return (nil, nil)
    }
    
    return response(responseSerializer: responseSerializer,
      completionHandler: completionHandler)
  }
}

Alamofire 1.3 added generic response serializers. So what we can do here is create one of these new lovely little objects and use a block to specify just what our slightly less generic serializer should do. Notice again that it works with an array of items of some generic class: GenericResponseSerializer<[T]>. If we were parsing an object (like maybe if we had a Quotes class), then we’d use GenericResponseSerializer<T> to indicate that it’s working with a generic class, not an array.

So what do we really need to do now? Well, we’ve got the data from the HTTP response and our completion handler is expecting an array of StockQuoteItems or maybe an error. So this is where we can put our JSON parsing code:


extension Alamofire.Request {
  public func responseArrayAtPath<T: ResponseJSONObjectSerializable>(pathToArray: [String]?, completionHandler: (NSURLRequest, NSHTTPURLResponse?, [T]?, NSError?) -> Void) -> Self {
    let responseSerializer = GenericResponseSerializer<[T]> { request, response, data in
      if let responseData = data
      {
        // Convert data to JSON
        var jsonError: NSError?
        let jsonData:AnyObject? = NSJSONSerialization.JSONObjectWithData(responseData, options: nil, error: &jsonError)
        if jsonData == nil || jsonError != nil
        {
          return (nil, jsonError)
        }

        // Use SwiftyJSON
        let json = SwiftyJSON.JSON(jsonData!)

        // Navigate to the array using the pathToArray
        var currentJSON = json
        if let path = pathToArray {
          for pathComponent in path {
            currentJSON = currentJSON[pathComponent]
          }
        }

        var objects: [T] = []
        // Use each JSON object in the array to create a model class objet
        for (index, item) in currentJSON {
          if let object = T(json: item)
          {
            objects.append(object)
          }
        }
        return (objects, nil)
      }
      // TODO: handle & return appropriate error(s)
      return (nil, nil)
    }
    
    return response(responseSerializer: responseSerializer,
      completionHandler: completionHandler)
  }
}

A little work here but nothing too exciting. We convert the data to Swift’s native JSON type using NSJSONSerialization (checking for errors, of course). If that succeeds we can use SwiftyJSON to deal with the JSON. Then we can use the elements in pathToArray to navigate into the JSON to find the array that we want.

Once we get to that array, we can take each object and use it to create an object of our generic class (for us each of those objects will be a StockQuoteItem). That’s why we needed that init function in the protocol: we needed to know that whatever T is instances of it can be created from JSON. And we can use the completion handler to send those newly created objects back to the caller.

And that’s all that we need for setting up the API call and the JSON parsing. We’ve got a way to fire off the GET request and get back an array of stocks quotes. Now how do we display them in a UIPageViewController?

Stock Display View Controller

The second item in our list of needs above was a view controller that knows how to display. It’ll be pretty simple, just a big label to stick some text in and enough code to handle that.

Add a new file to your project called SinglePageViewController.swift. Toss in a little code so it has a StockQuoteItem and will fill in an IBOutlet UILabel with text about the StockQuoteItem:


import UIKit

class SinglePageViewController: UIViewController {
  @IBOutlet weak var dataLabel: UILabel!
  var stock: StockQuoteItem?
  
  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    
    if let aSymbol = stock?.symbol, anAsk = stock?.ask, high = stock?.yearHigh, low = stock?.yearLow {
      dataLabel.text = aSymbol + ": " + anAsk + "\n" + "Year High: " + high + "\n" + "Year Low: " + low
    } else {
      dataLabel.text = stock?.symbol
    }
  }
}

Now we’ll design that view. Here’s a video in case the steps aren’t clear:

  • Open the storyboard
  • Drag a new UIViewController onto your storyboard
  • Change it’s class to SinglePageViewController
  • Set it’s Storyboard ID to SinglePageViewController
  • Add a UILabel to the storyboard
  • Hook up the label to the IBOutlet for dataLabel
  • Set the label to have 0 lines, which means it’ll display however many lines we give it
  • Set the label’s constraints so it has a 20 pixel border around it (update the view’s frames if you want to see where it will sit)
  • Change the font to something pretty (maybe Heiti SC in 42 point font, centered)
  • This seems like a pretty fancy app, so make it have white text on a dark blue background
  • In the project’s settings, change the status bar to “Light Content” so the status bar won’t use black text

Oh, one more thing. There are 2 ways to define whether the status bar uses black or white text: for each view controller or for the whole app. What we set up earlier is for the whole app. But by default the app will assume we’re defining it for each view controller. So we need to change that:

  • Open up the info.plist file (under Supporting Files)
  • Right-click and add a new row
  • Change the title of the row to “View controller-based status bar appearance”
  • Make sure it’s set to NO

Ok, so now we have a view controller that we can use. The last piece of the puzzle is holding the stock quote items somewhere that we can access to generate the view controllers when we need them.

A Data Controller

To get in the habit of keeping our code clean and well organized we’ll create another class. Add a new StocksDataController.swift file with a StocksDataController class that’ll be a wrapper around our array of stocks. This will be roughly an MVVM pattern with the StocksDataController as the View Model.

Our data controller needs to do a few things:

  • Load the stock quotes from the API using our getFeedItems function
  • Hold the stock quotes, probably in an array
  • Let us get the stock quotes for each view controller by index
  • Let us get the index for a stock quote (so we can figure out which one’s next or previous)

Don’t worry if those requirements weren’t obvious. You could end up with just the same code if you started trying to implement your custom UIPageViewController and saw how you needed to interact with the data controller. Sometimes building out one piece of code is the best way to figure out how another piece needs to work.

So let’s implement those requirements:


class StocksDataController {
  var dataObjects = NSMutableArray() // NSMutableArray because Swift arrays don't have objectAtIndex
  ...

Since we’re going to want to get the index for a stock item, we’ll use an NSMutableArray. Swift arrays are great but they don’t have an indexOfObject function. Instead of writing our own we’ll rely on the class that does have the functionality that we need.


  ...
  func loadStockQuoteItems(completionHandler: (NSError?) -> Void) {
    let symbols = ["AAPL", "GOOG", "YHOO"]
    StockQuoteItem.getFeedItems(symbols, completionHandler:{ (items, error) in
      if error != nil {
        completionHandler(error)
        return
      }
      if let stocks = items {
        for stock in stocks { // because we're getting back a Swift array but it's easier to do the PageController in an NSMutableArray
          self.dataObjects.addObject(stock)
        }
      }
      // success
      completionHandler(nil)
    })
  }

To fetch the stock quote items we just pass the symbols for the stocks we’re interested in and then save the results. Since the completion handler returns a Swift array and we’re using an NSMutableArray, we have to copy the items over one by one. If we wanted to we could’ve written the completion handler using NSArray instead, but then we’d end up with a conflict when we try to reuse the code somewhere else. This class is the only place that we really need an NSArray so it’s the only place we’ll use an NSArray. Even our UIPageViewController will have no idea that we’re dipping our toes into less Swift-y waters.


  func indexOfStock(stock: StockQuoteItem) -> Int {
    return dataObjects.indexOfObject(stock)
  }
  
  func stockAtIndex(index: Int) -> StockQuoteItem? {
    if (index < 0 || index > dataObjects.count - 1) {
      return nil
    }
    return dataObjects[index] as? StockQuoteItem
  }
}

The indexOfStock and stockAtIndex functions aren’t very exciting. The only interesting bit is that stockAtIndex returns an optional StockQuoteItem?. That lets us handle out of bounds values by returning nil instead of crashing or having to bubble up an error.

That’s all that we need for our data controller. On to the last bit: displaying the data!

Getting the Data in the Pages

Now we can implement the data source methods in our custom UIPageViewController using the data controller. First we’ll need to add an instance of our data controller to that class:


class GrokPageViewController: UIPageViewController, UIPageViewControllerDataSource {  
  let dataController = StocksDataController()
  ...

And implement the viewControllerAtIndex convenience method:


  func viewControllerAtIndex(index: Int) -> UIViewController? {
    if let stock = dataController.stockAtIndex(index) {
      let storyboard = UIStoryboard(name: "Main", bundle: nil)
      let vc = storyboard.instantiateViewControllerWithIdentifier("SinglePageViewController") as! SinglePageViewController
      vc.stock = stock
      return vc
    }
    return nil
  }

It gets the stock quote from the data controller for the index that we want. Then it creates a new instance of the SinglePageViewController using the storyboard (cuz that’s where we designed it). Finally it gives the SinglePageViewController the stock quote to display and returns it.

Now we can implement our data source methods, built on viewControllerAtIndex:


  func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
    if let currentPageViewController = viewController as? SinglePageViewController, currentStock:StockQuoteItem = currentPageViewController.stock {
      let currentIndex = dataController.indexOfStock(currentStock)
      return viewControllerAtIndex(currentIndex - 1)
    }
    return nil
  }
  
  func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
    if let currentPageViewController = viewController as? SinglePageViewController, currentStock:StockQuoteItem = currentPageViewController.stock {
      let currentIndex = dataController.indexOfStock(currentStock)
      return viewControllerAtIndex(currentIndex + 1)
    }
    return nil
  }

Both get the current stock quote from the current view controller and ask the data controller for its index. Then they use viewControllerAtIndex to get the next/previous view controller.

One line in there looks a bit long and confusing:


if let currentPageViewController = viewController as? SinglePageViewController, currentStock:StockQuoteItem = currentPageViewController.stock {

As of Swift 1.2 we can do multiple if-let statements on a single line, even using the results of the first statement in later ones, like:


if let a = b as? AClass, c = b.someFunction {
  ...
}

That’s equivalent to the following code but without the potential for ugly “pyramid code” that ends up pushed way to the right of the editor:


if let a = b as? AClass {
  if let c = b.someFunction {
    ...
  }
}

It seems a bit tightly spaced at first but it is really nice once you get used to it. It still makes the code a little harder to scan but that’s more because you have several conditions, not because of the syntax in which they’re shown.

Finally we need to tell the data controller to fetch the results when the view controller is shown:


  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    self.dataSource = self

    dataController.loadStockQuoteItems { (error) in
      if error != nil {
        var 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)
      } else {
        // set first view controller to display
        if let firstVC = self.viewControllerAtIndex(0) {
          let viewControllers = [firstVC]
          self.setViewControllers(viewControllers, direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
        }
      }
    }
  }

So we tell the data controller to load the quotes. If it runs into issues then we’ll display the error. If it succeeds (which is usually what no error means) then we’ll get the first view controller and display it. UIPageViewControllers are kind of daft, they won’t display the first view controller until you explicitly tell them to.

Now build & run and play with your API-backed page view controller.

But wait, there’s more! That daftness means we can do some nice UI: we can show a temporary loading screen until we have the stock quotes loaded.

Telling ‘Em What’s Going On

A golden rule in mobile is to always keep the user up to date. You never, ever, ever want them to think that your app has frozen or to be unsure about what it’s doing. We’ve got a pretty huge issue with our app: when it launches the user is stuck staring at a black screen until the stock quotes load. That’s bad enough that Apple would probably reject the app :(

So let’s add a view controller that we can put in the UIPageViewController until the stock quotes load. That way our users will know that the app is fetching the current stock quotes for them.

Here’s a video for these steps since it seems like such a long list:

  • Open up that storyboard again and drag in another UIViewController
  • Set its StoryboardID to PlaceHolderViewController (leave the class as UIViewController)
  • Drag a label and an activity indicator into the view
  • Make the activity indicator use the large white style
  • Make the activity indicator start spinning as soon as it’s shown
  • Make the label’s text “Loading Stock Quotes…”
  • Make the label’s font the same as the label in the SinglePageViewController (Heiti SC centered) but a little smaller to fit better (maybe 34pt)
  • Make the view and label’s color match the SinglePageViewController (white on dark blue)
  • Center the label
  • Position the activity indicator 20 pixels above the label, centered

Now in the code we’ll create an instance of this view controller and stick it in the UIPageViewController when we start up:


class GrokPageViewController: UIPageViewController, UIPageViewControllerDataSource {
  let dataController = StocksDataController()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    self.dataSource = self

    // show progress indicator or some indication that we're doing something
    // by loading an initial single view controller with a placeholder view
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let placeholderVC = storyboard.instantiateViewControllerWithIdentifier("PlaceHolderViewController") as! UIViewController
    self.setViewControllers([placeholderVC], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
    
    dataController.loadStockQuoteItems{ (error) in
      ...
    }
  }

  ...
}

And that’s all we need to do for that pretty loading view. Save & run to make sure your whole app works now.

If you don’t like the page curl animation (which does look kinda funny with the blue pages) you can change it to scroll in the storyboard under “Transition Style” for the UIPageViewController.

Want to make it even better? Here’s a challenge: modify this loading screen to handle not having a network connection (or any failure when fetching the stock quotes). So if we get an error back in loadStockQuoteItems then make the activity indicator stop spinning and give the user a way to restart the data fetch (maybe a button to call loadStockQuoteItems again). You should make sure that they can’t cause that refetch to happen again once it’s started (maybe hide that button while you’re fetching).

If you want to get really fancy, try building a weather app with a page for each location. There are lots of APIs to choose from for that kind of data.

And That’s All

We built a page view controller that displays data pulled from an API. It’s a lot quicker to say than to code, isn’t it? If you got tired of typing, here’s the final code on GitHub.

Today’s post was written in response to a comment on a previous post. Chris asked about UIPageViewControllers and I figured if one person was asking then more people were probably interested. So if you’ve got a challenge you’re working on and these tutorials don’t quite cover what you need, let me know in the comments below or by email.

If you’d like more Swift tutorials on topics like this one, sign up below to get them sent directly to your inbox.