June 25, 2015
We’ve set up a few different Swift tableviews that show results from a webservice:
- Hooking Up a REST API to a UITableView in Swift
- Pull to Refresh Table View in Swift
- Loading UITableViewCell Images from an API
Today we’ll improve on one of those projects to let the user choose different results to display in the table view. We’ll extend the Pull to Refresh Table View to have a segmented control so they can choose from a few different types of stocks to display, instead of just Apple, Google & Yahoo. Here’s what it’ll look like when we’re done:
Our Plan
Here’s what we’ll accomplish today, starting from the Pull to Refresh Table View project:
- Add a variable to our view controller that lets us choose between 3 types of stocks to display: tech, cars, and telecom. We’ll use an enumeration (aka enum) to represent these options
- Modify the Alamofire code that makes the API calls to let us pass in the stock symbols we want to show as an array of strings
- Add a segmented control to the tableview and use it to select between the 3 stock type options
- Fix up the table view so that tapping on the different stock types reloads the table view with the data that we want to see. We’ll need to make sure it works correctly with the pull to refresh feature but since the existing code is pretty clean that’ll be easy
To get started, we need the existing code from PullToUpdateDemo on GitHub. In terminal, get the branch from that tutorial:
git clone git@github.com:cmoulton/PullToUpdateDemo.git
cd PullToUpdateDemo
git checkout pull-to-refresh-swift-table-view
It’s a good idea to start a new branch for a new feature (especially since that last command took us off of the main git branch):
git checkout -b multiple_endpoints_branch
Run pod install
after cloning the repo to grab the cocoapods:
pod install
(If haven’t installed cocoapods yet you’ll need to run sudo gem install cocoapods
first.)
Finally we’ve got all of the code from the previous tutorial. Open the .xcworkspace
file in Xcode:
open PullToUpdateDemo.xcworkspace
If you just want the final code, here it is: PullToUpdateDemo with Multiple Endpoints on GitHub or [zip][6].
Representing the Choice
In the PullToUpdateDemo’s ViewController.swift, we want to add a new variable that represents which type of stock we want to see:
var stockType: *SomeType* = *Default Choice*
This variable will be used when we make the API call so we know which stock symbols to request. Technically we could just use the currently selected index of the segmented control. But then every time we see it we’d have to remember what that index represents.
To make it more clear we can use an enumeration for the type:
enum StockType {
case Tech
case Cars
case Telecom
}
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
var stockType: StockType = .Tech
...
Our StockType
type is an enum with 3 options: Tech, Cars and Telecom. To see how much cleaner this approach is let’s implement the function in the View Controller to get the stock symbols as array of strings for each stock type:
func symbolsStringForCurrentStockType() -> Array<String>
{
switch self.stockType {
case .Tech:
return ["AAPL", "GOOG", "YHOO"]
case .Cars:
return ["GM", "F"]
case .Telecom:
return ["T", "VZ", "CMCSA"]
}
}
If we hadn’t bothered to use a StockType enum, we’d have this code instead:
func symbolsStringForCurrentStockTypeIndex() -> Array<String>?
{
switch selectedIndex {
case 0:
return ["AAPL", "GOOG", "YHOO"]
case 1:
return ["GM", "F"]
case 2:
return ["T", "VZ", "CMCSA"]
default:
println("Segment index out of known range, do you need to add to the enum or switch statement?")
return nil
}
}
Not only is it harder to remember what selectedIndex == 1
means versus stockType == .Cars
, we end up dealing with an optional that really shouldn’t be an optional.
Our switch statement doesn’t know that we only have 3 options. Since the selectedIndex
is an integer we can’t make an exhaustive set of options. So we have to include a default:
case. But we can’t return a valid array of strings for that case. So we’re stuck changing the return type of the function from Array<String>
to Array<String>?
so we can return nil. And really, we know that the default case will never be reached so it’s not really an optional :(
Aside: We could return an empty array instead of nil but then we’d be putting the responsibility on the caller to know that’s a possibility. It’s just nicer to explicitly declare that the function might not return anything useful by declaring the return type as optional.
Optionals are a very useful part of Swift but they do add complexity. Only use optionals if you really need them. It’s easy to get in the habit of making everything an optional but your code will be much easier to read and update if you resist that temptation.
Aside: Why is this function in the View Controller instead of passing the StockType
? It’s just one more way to keep the code flexible. Since the view controller is the one deciding which stocks to look up, having it just pass the stock symbols to the API calling code means that we can write the API code to work with any array of stock symbol strings. Then when our client or boss comes by to ask us to add more options or to change the UI so users can type in stock symbols, we only need to make changes to the View Controller file. The harder to test API code won’t need to be touched at all.
User Interface
So we can represent the choice and convert the choice to a format that’s nice for the API calls. How about letting the user actually change the stockType?
Open up the main storyboard and drag a segmented control onto the nav bar (or in to the tableview header or footer, wherever you like). In the segmented control’s options (in the right panel’s 4th tab), set the number of segments to 3 and fill in the titles for the 3 segments as “Tech”, “Cars” and “Telecom”:
We need our view controller to get notified when the user taps the segmented control.
With the storyboard still open, switch to the Assistant Editor so you can see the storyboard and the view controller at once (the Assistant Editor button is in the top right corner of the Xcode screen, it’s the one with the two overlapping circles). Drag a connection from the segmented control’s Value Changed event (in the right panel’s 6th tab) into the View Controller and it’ll generate the method signature for your IBAction:
When the options dialog pops up, type in a descriptive name and set the sender type to UISegmentedControl
so we don’t have to cast it:
Now switch back to the Standard Editor (the button in the top right that looks like lines of text, next to the Assistant Editor button). Open the View Controller’s file to implement the IBAction when the segmented control’s value changes. We need to do 2 things:
- Update our stockType variable
- Tell the tableview to reload
@IBAction func stockTypeSegmentedControlValueChanged(sender: UISegmentedControl)
{
// Update our stockType variable
switch sender.selectedSegmentIndex {
case 0:
self.stockType = .Tech
case 1:
self.stockType = .Cars
case 2:
self.stockType = .Telecom
default:
println("Segment index out of known range, do you need to add to the enum or switch statement?")
}
// Tell the tableview to reload
refresh(sender)
}
Remember from the pull to refresh tutorial that the refresh
function just refetches the data from the API:
func refresh(sender:AnyObject)
{
self.loadStockQuoteItems()
}
What we haven’t done yet is actually pass our stock symbols to the code that makes the API call. Let’s add that, then we’ll update the API call code to use it. To do so, we need to get the symbols and then pass them to StockQuoteItem.getFeedItems
:
func loadStockQuoteItems() {
let symbols = symbolsStringForCurrentStockType()
StockQuoteItem.getFeedItems(symbols, completionHandler: { (items, 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)
}
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()
})
}
Since all of the stock API results will be the same, we don’t need to change anything other than those two lines.
API Call
If we try to compile the code now we’ll get an error about the arguments in StockQuoteItem.getFeedItems
. We need to update that function to accept our array of stock symbol strings.
Currently it looks like this:
class func getFeedItems(completionHandler: (Array<StockQuoteItem>?, NSError?) -> Void) {
Alamofire.request(.GET, self.endpointForFeed())
.responseItemsArray { (request, response, itemsArray, error) in
if let anError = error
{
completionHandler(nil, error)
return
}
completionHandler(itemsArray, nil)
}
}
What needs to change? Well, the parsing won’t change but the API URL needs to change to include the stock symbols, so we want:
class func getFeedItems(symbols: Array<String>, completionHandler: (Array<StockQuoteItem>?, NSError?) -> Void) {
Alamofire.request(.GET, self.endpointForFeed(symbols))
.responseItemsArray { (request, response, itemsArray, error) in
if let anError = error
{
completionHandler(nil, error)
return
}
completionHandler(itemsArray, nil)
}
}
endpointForFeed
is going to get a bit more complicated. Before we could just hardcode the whole URL. Now we’ll have to build it up from our input. The URL is composed of two parts: the base API query endpoint and the parameters (mostly the query that we want to run):
Here’s the query we were using for the tech symbols
select symbol, Ask, YearHigh, YearLow from yahoo.finance.quotes where symbol in ("AAPL", "GOOG", "YHOO")
We also need to add on a few specifiers to tell the API where to look for the data table and what format we want:
&format=json&env=http://datatables.org/alltables.env
The whole URL looks like:
https://query.yahooapis.com/v1/public/yql?q=*URL encoded query and specifiers*
E.g.:
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
To create the URL we need to:
- Put our array of strings together into a string like
AAPL", “GOOG”, “YHOO
- Stick that string into the query & specifiers to get something like
select … where symbol in (“AAPL”, “GOOG”, “YHOO")&format=json&env=http://datatables.org/alltables.env
- URL encode the query & specifiers
- Add
https://query.yahooapis.com/v1/public/yql?q=
Like so:
class func endpointForFeed(symbols: Array<String>) -> String {
// Put our array of strings together
let symbolsString:String = "\", \"".join(symbols)
// Stick that string into the query & specifiers
let query = "select * from yahoo.finance.quotes where symbol in (\"\(symbolsString) \")&format=json&env=http://datatables.org/alltables.env"
// URL encode the query & specifiers
let encodedQuery = query.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
// Add the yahoo APIs URL:
let endpoint = "https://query.yahooapis.com/v1/public/yql?q=" + encodedQuery!
return endpoint
}
And That’s All
Save & run. You can tap the segmented control and see the stocks change. Test that the pull to refresh still works (it should, since the refresh
and loadStockQuoteItems
work with the current stockType to use the currently selected stocks.
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 with Multiple Endpoints on GitHub.