Вы находитесь на странице: 1из 241

iOS Apps with REST APIs

Building Web-Driven Apps in Swift

Christina Moulton

©2015 Teak Mobile Inc. All rights reserved. Except for the use in any review, the reproduction or
utilization of this work in whole or in part in any form by any electronic, mechanical or other
means is forbidden without the express permission of the author.
Tweet This Book!
Please help Christina Moulton by spreading the word about this book on Twitter!
The suggested hashtag for this book is #SwiftRestAppsBook.
Find out what other people are saying about the book by clicking on this link to search for this
hashtag on Twitter:
https://twitter.com/search?q=#SwiftRestAppsBook
Contents

Thanks i

1. From JSON API to Swift App 1


1.1 What Will You Be Able to Do? 1
1.2 Who Is This Book For? 2
1.3 Who Is This Book Not For? 2
1.4 Using This Book 2
1.5 What We Mean By Web Services / APIs / REST / CRUD 3
1.6 JSON 3
1.7 Versions 3
1.8 Source Code 4
1.9 Disclaimer 4
1.10 Trademarks 4

2. Our App’s Requirements 6


2.1 Match Tasks to Endpoints 7
2.2 User Interface 9
2.3 API Requirements 10
2.4 Make a Plan 11

3. Swift JSON Parsing & Networking Calls 101 13


3.1 Simple REST API Calls with Swift 13
3.2 REST API Calls with Alamofire & SwiftyJSON 22
3.3 Alamofire Router 27
3.4 Strongly Typed GET and POST Calls with Alamofire 33
3.5 And That’s All 42

4. Why I Use Libraries Like Alamofire 43

5. Hooking Up a REST API to a Table View 44


5.1 Our Swift Project 44
5.2 Analyzing the API JSON Response 47
5.3 Setting Up the Table View 52
5.4 Getting & Processing the API Response 60
5.5 And That’s All 66
CONTENTS

6. Custom Headers 68
6.1 Session Headers 68
6.2 Per Request Headers 69
6.3 Headers in URLRequestConvertible 69
6.4 And That’s All For Headers 70

7. Loading UITableViewCell Images from an API 71


7.1 Loading UIImages from URLs 72
7.2 UITableViewCell Images from URLs 72
7.3 Enhancements 75
7.4 Caching Images 76
7.5 A Better Cache: PINRemoteImage 80
7.6 And That’s All 81

8. Pagination, a.k.a., Load More on Scroll 82


8.1 Where is the Next Page? 82
8.2 Fetching and Appending 85
8.3 Integrating with the View Controller 87
8.4 When to Load More Gists? 89
8.5 And That’s All for Pagination 91

9. Pull to Refresh 92
9.1 Adding Pull to Refresh 92
9.2 Showing the Last Refreshed Time 94
9.3 And That’s All 96

10. Authentication 97
10.1 The Docs 97
10.2 Basic Auth: Username/Password 97
10.3 HTTP Header Authentication 102
10.4 Alamofire Validation 104
10.5 OAuth 2.0 106
10.6 Displaying the Results 137
10.7 Unauthorized Responses: 404 vs 401 149
10.8 And That’s All 151

11. Switching Lists 152


11.1 Setting Up the UI 152
11.2 Sharing a Completion Handler 156
11.3 And That’s All 160

12. Switching Between View Controllers and More JSON Parsing 161
12.1 JSON Parsing: Arrays & Dates 161
12.2 Parsing Dates in JSON 164
CONTENTS

12.3 Configuring the Detail View Controller 166


12.4 Passing Data in a Segue 171
12.5 Adding a Table View 172
12.6 Displaying Gist File Content 178
12.7 And That’s All 179

13. Adding More API Calls - Starring 180


13.1 Is the Gist Starred? 180
13.2 Starred Status in the Table View 181
13.3 PUT and DELETE Calls to Star and Unstar Gists 184
13.4 Authorization Check 186
13.5 And That’s All 190

14. Deleting Gists 191


14.1 DELETE API Call 191
14.2 User Interface: Table View Delete Features 192
14.3 And That’s All 195

15. Creating Gists and Clearing the Cache 196


15.1 POST API Call with Nested JSON Parameters 196
15.2 Creating an Input Form with Validation 200
15.3 And That’s All 208

16. What if They’re Offline? 209


16.1 How Do We Know? 209
16.2 Save a Local Copy 221
16.3 Databases 227

17. What Next? 229


17.1 User Interface 229
17.2 Test the User Experience 229
17.3 Suggested Exercises 229
17.4 Did I Miss Anything? 230

A Brief Introduction to CocoaPods 231


Adding a CocoaPod to a Project 231
What Does the Podfile Mean? 232
Other Options 233
Dependencies 233
CocoaPods Version Numbers 233
Updating CocoaPods 234
Thanks
Without a few key people this book wouldn’t have happened. Most of all, thanks to Jeff Moulton
for putting up with my excessive focus on coding & writing, even while living on a 34’ sailboat. Jeff
also took the cover photo.
Thanks also to:

• My Twitter peeps for support & fav’s


• @BugKrusha¹ & the iOS Developers² community
• GitHub
• LeanPub
• Everyone who gave feedback or asked questions about the book or GrokSwift.com³. Every
little bit helps make the book better for you.

¹http://twitter.com/BugKrusha
²http://ios-developers.io
³https://grokswift.com

i
1. From JSON API to Swift App
You need to build an iOS app around your team’s API or integrate a third party API. You need a quick,
clear guide to demystify Xcode and Swift. No esoteric details about Core Anything or mathematical
analysis of flatMap. Only the nitty gritty that you need to get real work done now: pulling data from
your web services into an iOS app, without tossing your MacBook or Mac Mini through a window.
You just need the bare facts on how to get CRUD done on iOS. That’s what this book will do for
you.

1.1 What Will You Be Able to Do?


After reading this book you’ll be able to:

• Analyze a JSON response from a web service call and write Swift code to parse it into model
objects
• Display those model objects in a table view so that when the user launches the app they have
a nice list to scroll through
• Add authentication to use web service calls that require OAuth 2.0, a username/password, or
a token
• Transition from the main table view to a detail view for each object, possibly making another
web service call to get more info about the object
• Let users add, modify and delete objects (as long as your web service supports it)
• Hook in to more web service calls to extend you app, like adding user profiles or letting users
submit comments or attach photos to objects

To achieve those goals we’ll build out an app based on the GitHub API, focusing on gists. (If you’re
not familiar with gists, they’re basically just text snippets, often code written a GitHub user.) Your
model objects might be bus routes, customers, chat messages, or whatever kind of object is core to
your app. We’ll start by figuring out how to make API calls in Swift then we’ll start building out our
app one feature at a time:

• Show a list of all public gists in a table view


• Load more results when the user scrolls down
• Let them pull to refresh to get the latest public gists
• Load images from URLs into table view cells
• Use OAuth 2.0 for authentication to get lists of private and starred gists

1
From JSON API to Swift App 2

• Have a detail view for each gist showing the text


• Allow users to add new gists, star and unstar gists, and delete gists
• Handle not having an internet connection with warnings to the user and saving the gists on
the device

1.2 Who Is This Book For?


• Software developers getting started with iOS but experienced in other languages
• Front-end devs looking to implement native UIs for iOS apps (no CSS, oh noes!)
• Back-end devs tasked with getting the data into the user’s hands on iOS
• Android, Windows Phone, Blackberry, Tizen, Symbian & Palm OS devs looking to expand
their web service backed apps to iOS
• Anyone whose boss is standing over their shoulder asking why the API data isn’t showing up
in the table view yet

1.3 Who Is This Book Not For?


• Complete newcomers to programming, you should have a decent grasp of at least one object-
oriented programming language or have completed several intro to iOS tutorials
• Designers, managers, UX pros, … It’s a programming book. All the monospace font inserts
will probably drive you crazy.
• Cross-platform developers dedicated to their tools (including HTML5 & Xamarin), this is all
Swift & native UI, all the time
• Programmers building apps that have little or no web service interaction
• Game devs, unless you’re tying in a REST-like API

1.4 Using This Book


This book is mostly written as a tutorial in implementing the gists app. Depending on how you learn
best and how urgently you need to implement your own app, there are two different approaches you
might take:

1. Work through the tutorials as written, creating an app for GitHub Gists. You’ll understand
how that app works and later be able to apply it to your own apps.
2. Read through the tutorials but implement them for your own app and API. Throughout the
text I’ll point out where you’ll need to analyze your own requirements and API to help you
figure out how to modify the example code to work with your API. Those tips will look like
this:
From JSON API to Swift App 3

List the tasks or user stories for your app. Compare them to the list for the gists app, focusing
on the number of different objects (like stars, users, and gists) and the types of action taken
(like viewing a list, viewing an object’s details, adding, deleting, etc.).

We’ll start with that task in the next chapter. We’ll analyze our requirements and figure out just what
we’re going to build. Then we’ll start building the gists app, right after an introduction to making
network calls and parsing JSON in Swift.

1.5 What We Mean By Web Services / APIs / REST /


CRUD
Like anything in tech there are plenty of buzzwords around web services. For a while it was really
trendy to say your web services were RESTful. If you want to read the theory behind it, head over
to Wikipedia¹. For our purposes in this book, all we mean by “REST web service” or even when we
say “web service” or “API” is that we can send an HTTP request and we get back some data in a
format that’s easy to use in our app. Usually the response will be in JSON.
Web services are wonderful since they let you use existing systems in your own apps. There’s always
a bit of a learning curve with any web service that you’re using for the first time since every one
has its own quirks. Most of the integration is similar enough that we can generalize how to integrate
them into our iOS apps.
If you want an argument about whether or not a web service is really RESTful you’re not going to
find it here. We’ve got work that just needs to get done.

1.6 JSON
In this book we’re going to deal with web services that return JSON. JSON is hugely common these
days so it’s probably what you’ll be dealing with. Of course, there are other return types out there,
like XML. This book won’t cover responses in anything but JSON but it will encapsulate the JSON
parsing so that you can replace it with whatever you need to without having to touch a ton of code.
If you are dealing with XML response you should look at NSXMLParser².

1.7 Versions
This is version 1.1.1 of this book. It uses Swift 2.0, iOS 9, and Xcode 7.1. When we use libraries we’ll
explicitly list the versions used. The most commonly used ones are Alamofire 3.1 and SwiftyJSON
2.3.
¹https://en.wikipedia.org/wiki/Representational_state_transfer
²https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSXMLParser_Class/
From JSON API to Swift App 4

Version 1.0 of this book used Alamofire 2.0 and SwiftyJSON 2.2. Changes to the code between
versions 1.0 and this version can be found on GitHub³.

1.8 Source Code


All sample code is available on GitHub⁴ under the MIT license⁵. Links are provided throughout the
text. Each chapter has a tag allowing you to check out the code in progress up to the end of that
chapter.
Individuals are welcome to use code for commercial and open-source projects. As a courtesy, please
provide attribution to “Teak Mobile Inc.” or “Christina Moulton”. For more information, review the
complete license agreement in the GitHub repo⁶.

1.9 Disclaimer
The information provided within this eBook is for general informational purposes only. The author
has made every effort to ensure the accuracy of the information within this book was correct at
time of publication. Teak Mobile Inc. and/or Christina Moulton do not assume and hereby disclaims
any liability to any party for any loss, damage, or disruption caused by errors or omissions, whether
such errors or omissions result from accident, negligence, or any other cause.
Teak Mobile Inc. and/or Christina Moulton shall in no event be liable for any loss of profit or any
other commercial damage, including but not limited to special, incidental, consequential, or other
damages.
Any use of this information is at your own risk.

1.10 Trademarks
This book identifies product names and services known to be trademarks, registered trademarks, or
service marks of their respective holders. They are used throughout this book in an editorial fashion
only. In addition, terms suspected of being trademarks, registered trademarks, or service marks have
been appropriately capitalized, although Teak Mobile Inc. and Christina Moulton cannot attest to
the accuracy of this information. Use of a term in this book should not be regarded as affecting the
validity of any trademark, registered trademark, or service mark. Teak Mobile Inc. and/or Christina
Moulton are not associated with any product or vendor mentioned in this book.
Apple, Xcode, App Store, Cocoa, Cocoa Touch, Interface Builder, iOS, iPad, iPhone, Mac, OS X, Swift,
and Xcode are trademarks of Apple, Inc., registered in the United States and other countries.
³https://github.com/cmoulton/grokSwiftREST/compare/Alamofire3
⁴https://github.com/cmoulton/grokSwiftREST_v1.1
⁵https://opensource.org/licenses/MIT
⁶https://github.com/cmoulton/grokSwiftREST_v1.1/blob/master/grokSwiftREST/LICENSE.txt
From JSON API to Swift App 5

GitHub is a trademark of GitHub, Inc. registered in the United States.


Mashape is a trademark of Mashape, Inc. registered in the United States.
2. Our App’s Requirements
It’s always tempting to jump right into coding but it usually goes a lot smoother if we plan it out in
advance. At the least we need some idea of what we’re building. Let’s lay that out for the gists app
and you can modify it to suit your app.
The first thing to do is to figure out what screens or views our app will have. There are a few ways
to do this task but I prefer to make a list of things that users will want to do with your app then
design the screens to make those things easy.
So what do people do with gists? Gists are snippets of text, often bits of code that are easily shared.
So people might:

1. Look at a list of public gists to see what’s new


2. Search for interesting gists, maybe by programming language
3. Star a gist so they can find it later
4. Look at a list of gists they’ve starred
5. Look at a list of their own gists to grab code they commonly use but don’t want to retype all
the time
6. Look at details for a gist in a list (public, their own, or starred)
7. Create a new gist
8. Delete one of their gists

List the tasks or user stories for your app. Compare them to the list for the gists app, focusing
on the number of different objects (like stars, users, and gists) and the types of action taken
(like viewing a list, viewing an object’s details, adding, deleting, etc.).

You might end up with a really long list. Consider each item and whether it’s really necessary for the
first version of your app. Maybe it can be part of the next release if the first one gets some traction?

Evaluate each task on your list. Decide which ones will form v1.0 of your app. You might
even want to design v2.0 now so you’re not tempted to put everything in the first version.
A good shipped app is far better than a perfect app that’s indefinitely delayed.

6
Our App’s Requirements 7

2.1 Match Tasks to Endpoints


Next look at each of those tasks and figure out how you can use the API to accomplish them or to get
the data you’ll need to display. We’ll check the documentation for the GitHub gists API¹ to find the
endpoint for each task. We’ll make notes of anything special that we need to do, like authentication
or pagination.

2.1.1 List Public Gists


GET /gists/public

No authentication required. Will be paginated so we’ll have to load more results if they want to see
more than 20 or so.

2.1.2 Search Gists


Hmm, there isn’t an API for searching gists. Is our app still useful without search? I think so, so we
don’t need to abandon the project.

2.1.3 Star/Unstar a Gist


PUT /gists/:id/star
DELETE /gists/:id/star

Requires authentication.

2.1.4 List Starred Gists


GET /gists/starred

Requires authentication.

2.1.5 List my Gists


There are two ways to get a list of a user’s gists:

GET /users/:username/gists

Or, if authenticated in:


¹https://developer.github.com/v3/gists/
Our App’s Requirements 8

GET /gists

2.1.6 View Gist Details


We’ll probably be able to pass the data from the list of gists to the detail view but if we can’t then
we can get a single gist’s details:

GET /gists/:id

If we want to display whether a gist is starred then we can use:

GET /gists/:id/star

2.1.7 Create Gist


POST /gists

Requires authentication to create a gist owned by a user. Otherwise the gist is created anonymously.
The JSON to send to create a gist looks like:

{
"description": "the description for this gist",
"public": true,
"files": {
"file1.txt": {
"content": "String file content"
}
}
}

2.1.8 Delete Gists


DELETE /gists/:id

Requires authentication.
Those are the endpoints for our tasks. Other than not being able to build our search feature, we
shouldn’t have any trouble building our demo app around this API.

Analyze each action and list the API endpoint or iOS feature that will be needed for it.
Make sure that everything is possible using the API that’s available. If not and the API is
being built by your team then request what you need now so there’s plenty of time to get
it implemented.
Our App’s Requirements 9

2.2 User Interface


Now we have to figure out how we’re going to make the app usable by the users. Let’s look at each
task and figure out how we’d like it to work. I’ve reordered the tasks below a bit to group together
bits that will share parts of the interface.

2.2.1 Authentication Flow


Since there isn’t much they can do in the app without being logged in, we’ll check at launch if they’re
authenticated. If not we’ll start the login process right away.
If your API provides lots of functionality without authentication then you might want to delay
requiring the user to log in. If that’s the case you can add the authentication checks before making
the API calls that require authentication.

2.2.2 List Public Gists


On launch the user sees a list (table view) with the public gists.

2.2.3 List Starred Gists


From the public gists the user can switch to a similar list of my starred gists.

2.2.4 List My Gists


From the public or starred gists the user can switch to a similar list of their own gists.
Sounds like we’ll be able to use a single table view and have a selector so the user can pick which
of the 3 lists of gists they want to view.

2.2.5 View Gist Details


When they tap on a gist in one of the lists we’ll transition to a different view. That view will list
details about the gist (description and filenames) and let them view the text of the files. It’ll also
show whether we’ve starred the gist.

2.2.6 Star/Unstar a Gist


Within a gist’s detail view we’ll show the starred status. They will be able to tap to star or unstar a
gist in that view.
Our App’s Requirements 10

2.2.7 Create Gist


On the list of My Gists we’ll have a + button in the upper right corner. It’ll display a form where
they can enter the info for the new gist:

• Description: text
• isPublic: Boolean
• Filename: text
• File content: text

To keep it simple we’ll only allow a single file in gists created in the app in v1.0.

2.2.8 Delete Gists


We’ll allow swipe to delete on the list of My Gists.

Go through your tasks and figure out the user interface that people will use to accomplish
those tasks.

2.3 API Requirements


We’ll have some requirements to interact with the API that aren’t obvious when we consider the
user’s tasks. But reading through the documentation carefully can help us make a list.

2.3.1 Authentication
You can read public gists and create them for anonymous users without a token;
however, to read or write gists on a user’s behalf the gist OAuth scope is required.
GitHub Gists API docs²

So we’ll need to set up authentication, preferably OAuth 2.0, including the gist scope. The API will
work with a username/password but then we’d have to worry about securing that data. With OAuth
2.0 we never see the username & password, only the token for our app.
We will store the OAuth token securely.

Check your APIs authentication requirements. In the auth chapter we’ll cover how to im-
plement OAuth 2.0, token-based authentication, and basic auth with username/password.

²https://developer.github.com/v3/gists/#authentication
Our App’s Requirements 11

2.3.2 Set the Accept Header


There’s a note in the GitHub API docs³ that we should set the accept header like: Accept:
application/vnd.github.v3+json. Should is often code for “will break things later if you don’t” so
we’ll do that.

Check your APIs documentation for any required headers.

In iOS 9 Apple introduced Apple’s App Transport Security⁴. ATS requires SSL to be used for
transferring data and it’s pretty picky about just how it’s implemented. Sadly this means that a
lot of servers out there don’t meet ATS’s requirements. GitHub’s gist API complies with the ATS
requirements so we won’t have to add an exception.

If you find that you get SSL errors when calling your API from iOS 9 then you’ll probably
need to add an exception to ATS. See the Networking 101 chapter for details on adding that
exception. You can use the code in that chapter to try some simple API calls to your server
to see if you get SSL errors.

2.4 Make a Plan


Now that we know what we need to do we can figure out how we’re going to do it. We’ll build the
app up incrementally, feature by feature:

• Set up the app with a table view displaying the public gists
• Add custom headers
• Load images in table view cells
• Load more gists when they scroll down
• Add pull to refresh
• Add authentication and let them switch to displaying My Gist and Starred Gists
• Create a detail view for the gists
• Add starring & unstarring gists in the detail view
• Add deleting and creating gists
• Handle not having an internet connection
³https://developer.github.com/v3/
⁴https://developer.apple.com/library/prerelease/ios/technotes/App-Transport-Security-Technote/index.html#//apple_ref/doc/uid/TP40016240
Our App’s Requirements 12

Put your views and tasks in order to implement them. Try to match up roughly with the
order for the gists app. If you don’t have an API call to start with that doesn’t require
authentication you might need to jump ahead to the auth chapter before starting on the
table view chapter. If your API requires custom headers to be sent with all requests then
you’ll want to start with the headers chapter then come back to the table view chapter.

Now that we’ve sorted out the basic requirements for our app we know where to start. First we’ll
spend a little time looking at how to make web requests and parse JSON in Swift so we don’t get
bogged down with those details later.
3. Swift JSON Parsing & Networking
Calls 101
I was all ready to jump right in to some useful code for you then Apple introduced App Transport
Security¹ in iOS 9. While ATS should be a great feature for securing the data being sent to and from
your iPhone, it’s a bit of a pain as a developer right now.
ATS requires SSL to be used for transferring data and it is pretty picky about how it’s implemented.
Sadly this means that a lot of servers out there don’t meet the ATS requirements. So what can we
do if we need to work with one of these servers? Well, we’ll deal with that right now because the
Networking 101 code below uses a server that requires it.
We’ll have to add an exception to App Transport Security for that server. While we could just disable
ATS it’s much more secure to create an exception only for the one server that we need to access.
The API that we’ll be using in this chapter is at http://jsonplaceholder.typicode.com/² so that’s what
we’ll create the exception for.
To create the exception we’ll need to add some keys to the info.plist in our project. We’ll add
an NSAppTransportSecurity dictionary. It’ll contain an NSExceptionDomains dictionary with a
dictionary for the server: jsonplaceholder.typicode.com (note: no trailing slashes and no http
or https prefix). Within the jsonplaceholder.typicode.com dictionary we’ll have a boolean entry
NSThirdPartyExceptionAllowsInsecureHTTPLoads set to YES:

ATS exception settings

Ok, now we can actually get into the networking code.

3.1 Simple REST API Calls with Swift


Pretty much every app these days consumes or creates content through an API. In this book
we’ll mostly use Alamofire³, a rich networking library, but you can also use NSURLSessions’s
asynchronous data task requests for quick and dirty REST calls.
¹https://developer.apple.com/library/prerelease/ios/technotes/App-Transport-Security-Technote/index.html#//apple_ref/doc/uid/TP40016240
²http://jsonplaceholder.typicode.com/
³http://nshipster.com/alamofire/

13
Swift JSON Parsing & Networking Calls 101 14

The function to use to make an async URL request is:

public func dataTaskWithRequest(request: NSURLRequest,


completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void)
-> NSURLSessionDataTask

It takes a request which contains the URL then goes off and sends the request. Once it gets a response
(or has an error to report), the completion handler gets called. The completion handler is where
we can work with the results of the call: error checking, saving the data locally, updating the UI,
whatever. We’ll talk about completion handlers a bit more in a minute when we implement one for
dataTaskWithRequest
The simplest case is a GET request. Of course, we need an API to hit. Fortunately there’s super handy
JSONPlaceholder⁴:

“JSONPlaceholder is a fake online REST API for testing and prototyping. It’s like image
placeholders but for web developers.”

JSONPlaceholder has a handful of resources similar to what you’ll find in a lot of apps: users, posts,
photos, albums, … We’ll stick with posts.
First let’s print out the title of the first post. To get a single post, we need to make a GET call to
the posts endpoint with an ID number. Checking out http://jsonplaceholder.typicode.com/posts/⁵
we can see that the id for the first post is 1. So let’s grab it:
First, set up the URL request:

let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"


guard let url = NSURL(string: postEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = NSURLRequest(URL: url)

The guard statement lets us check that the URL we’ve provided is valid.
Then we need an NSURLSession to use to send the request:

let config = NSURLSessionConfiguration.defaultSessionConfiguration()


let session = NSURLSession(configuration: config)

Then create the data task:


⁴https://github.com/typicode/jsonplaceholder
⁵http://jsonplaceholder.typicode.com/posts/
Swift JSON Parsing & Networking Calls 101 15

let task = session.dataTaskWithRequest(urlRequest, completionHandler: nil)

And finally send it (yes, this is an oddly named function):

task.resume()

Calling this now will hit the URL (from the urlRequest) and obtain the results (using a GET request
since that’s the default). To actually get the results to do anything useful we need to implement the
completion handler.
Completion handlers can be a bit confusing the first time you run in to them. On the one hand,
they’re a variable or argument but, on the other hand, they’re a chunk of code. Weird if you’re not
used to that kind of thing (a.k.a., blocks or closures).
Completion handlers are super convenient when your app is doing something that might take a little
while, like making an API call, and you need to do something when that task is done, like updating
the UI to show the data. You’ll see completion handlers in Apple’s APIs like dataTaskWithRequest
and later on we’ll add some of our own completion handlers when we’re building out our API calls.
In dataTaskWithRequest the completion handler argument has a signature like this:

completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void

So it’s a code block (it must be if it has a return type which is what -> tells us). It has 3 arguments:
(NSData?, NSURLResponse?, NSError?) and returns nothing: Void. To specify a completion handler
we can write the code block inline like this:

let task = session.dataTaskWithRequest(urlRequest, completionHandler:


{ (data, response, error) in
// this is where the completion handler code goes
})
task.resume()

The block is the bit between the curly brackets. Notice that the 3 arguments in the block
(data, response, error) match the arguments in the completion handler declaration: (NSData?,
NSURLResponse?, NSError?). You can specify the types explicitly when you create your block but
it’s not necessary because the compiler can figure it out. Sometimes it’s good to remember that
people read code, not just computers, so it doesn’t hurt to be explicit:
Swift JSON Parsing & Networking Calls 101 16

let task = session.dataTaskWithRequest(urlRequest, completionHandler: { (data: NSData?,


response: NSURLResponse?, error: NSError?) in
// this is where the completion handler code goes
print(response)
print(error)
})
task.resume()

Somewhat confusingly, you can actually drop the completionHandler: bit and just tack the code
block on at the end of the function call. This is totally equivalent to the code above and a pretty
common thing you’ll see in Swift code:

let task = session.dataTaskWithRequest(urlRequest) { (data, response, error) in


// this is where the completion handler code goes
print(response)
print(error)
}
task.resume()

If you want to ignore some arguments you can tell the compiler that you don’t want them by
replacing them with _:

let task = session.dataTaskWithRequest(urlRequest) { (data, _, error) in


// can't do print(response) since we don't have response
print(error)
}
task.resume()

We can also declare the code block as a variable then pass it in when we call dataTaskWithRequest.
That’s handy if we want to use the same completion handler for multiple tasks. We will use this
technique when implementing an OAuth 2.0 login flow, since it has lots of steps but we will want
to handle any of them failing similarly.
Here’s how you can use a variable for a completion handler:
Swift JSON Parsing & Networking Calls 101 17

let myCompletionHandler: (NSData?, NSURLResponse?, NSError?) -> Void = {


(data, response, error) in
// this is where the completion handler code goes
print(response)
print(error)
}
let task = session.dataTaskWithRequest(urlRequest, completionHandler: myCompletionHandler)
task.resume()

What’ll happen to our little code block? Well, it won’t get called right away when we call
dataTaskWithRequest. But somewhere in Apple’s implementation of dataTaskWithRequest it will
get called like this:

public func dataTaskWithRequest(request: NSURLRequest,


completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void)
-> NSURLSessionDataTask {
// make an URL request
// wait for results
// check for errors and stuff
completionHandler(data, response, error)
// return the data task
}

You don’t need to write that in your own code, it’s already implemented in dataTaskWithRequest.
In fact, there are probably a few calls like that for handling success and error cases. The completion
handler will just sit around waiting to be called whenever dataTaskWithRequest is done.
So what’s the point of completion handlers? Well, we can use them to take action when something
is done. Like here we could set up a completion handler to print out the results and any potential
errors so we can make sure our API call worked. Let’s go back to our dataTaskWithRequest example
and implement a useful completion handler. Here’s where the code will go:

let task = session.dataTaskWithRequest(urlRequest, completionHandler:


{ (data, response, error) in
// do stuff with response, data & error here
})
task.resume()

Now we have access to 3 arguments: the URL response, the data returned by the request and an
error (if one occurred). So let’s check for errors and figure out how to get at the data that we want:
the first post’s title. We need to:

1. Make sure we got data and no error


Swift JSON Parsing & Networking Calls 101 18

2. Try to transform the data into JSON (since that’s the format returned by the API)
3. Access the post object in the JSON and print out the title

You’ll need to add import Foundation at the top of your file to have access to NSJSONSerialization.

let task = session.dataTaskWithRequest(urlRequest, completionHandler: {


(data, response, error) in
guard let responseData = data else {
print("Error: did not receive data")
return
}
guard error == nil else {
print("error calling GET on /posts/1")
print(error)
return
}
// parse the result as JSON, since that's what the API provides
let post: NSDictionary
do {
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options: []) as! NSDictionary
} catch {
print("error trying to convert data to JSON")
return
}
// now we have the post, let's just print it to prove we can access it
print("The post is: " + post.description)

// the post object is a dictionary


// so we just access the title using the "title" key
// so check for a title and print it if we have one
if let postTitle = post["title"] as? String {
print("The title is: " + postTitle)
}
})
task.resume()

Which prints out:


Swift JSON Parsing & Networking Calls 101 19

The post is: {


body = "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderi\
t molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto";
id = 1;
title = "sunt aut facere repellat provident occaecati excepturi optio reprehenderit";
userId = 1;
}
The title is: sunt aut facere repellat provident occaecati excepturi optio reprehenderit

It’s a little verbose but if you just need a quick GET call to an API without authentication, that’ll do
it.
If you need a method type other than GET then you’ll need to use a mutable NSURLRequest so you
can set the method type:

let postsEndpoint: String = "http://jsonplaceholder.typicode.com/posts"


let postsUrlRequest = NSMutableURLRequest(URL: NSURL(string: postsEndpoint)!)
postsUrlRequest.HTTPMethod = "POST"

Then we can set the new post as the HTTPBody for the request:

let newPost: NSDictionary = ["title": "Frist Psot", "body": "I iz fisrt", "userId": 1];
do {
let jsonPost = try NSJSONSerialization.dataWithJSONObject(newPost, options: [])
postsUrlRequest.HTTPBody = jsonPost
} catch {
print("Error: cannot create JSON from post")
}

Now we can execute the request (assuming we’re keeping the session that we created earlier around):

let task = session.dataTaskWithRequest(postsUrlRequest, completionHandler: nil)


task.resume()

If it’s working correctly then we should get our post back as a response along with the id number
assigned to it. Since it’s just for testing, JSONPlaceholder will let you do all sorts of REST requests
(GET, POST, PUT, PATCH, DELETE and OPTIONS) but it won’t actually change the data based on
your requests. So when we send this POST request, we’ll get a response with an ID to confirm that
we did it right but it won’t actually be kept in the database so we can’t access it on subsequent calls.
Swift JSON Parsing & Networking Calls 101 20

let newPost: NSDictionary = ["title": "Frist Psot", "body": "I iz fisrt", "userId": 1];
do {
let jsonPost = try NSJSONSerialization.dataWithJSONObject(newPost, options: [])
postsUrlRequest.HTTPBody = jsonPost

let config = NSURLSessionConfiguration.defaultSessionConfiguration()


let session = NSURLSession(configuration: config)

let task = session.dataTaskWithRequest(postsUrlRequest, completionHandler: {


(data, response, error) in
guard let responseData = data else {
print("Error: did not receive data")
return
}
guard error == nil else {
print("error calling GET on /posts/1")
print(error)
return
}

// parse the result as JSON, since that's what the API provides
let post: NSDictionary
do {
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options: []) as! NSDictionary
} catch {
print("error parsing response from POST on /posts")
return
}
// now we have the post, let's just print it to prove we can access it
print("The post is: " + post.description)

// the post object is a dictionary


// so we just access the title using the "title" key
// so check for a title and print it if we have one
if let postID = post["id"] as? Int
{
print("The ID is: \(postID)")
}
})
task.resume()
}

Deleting is pretty similar (minus creating the JSON post):


Swift JSON Parsing & Networking Calls 101 21

let firstPostEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"


let firstPostUrlRequest = NSMutableURLRequest(URL: NSURL(string: firstPostEndpoint)!)
firstPostUrlRequest.HTTPMethod = "DELETE"

let config = NSURLSessionConfiguration.defaultSessionConfiguration()


let session = NSURLSession(configuration: config)

let task = session.dataTaskWithRequest(firstPostUrlRequest, completionHandler: {


(data, response, error) in
guard let _ = data else {
print("error calling DELETE on /posts/1")
return
}
})
task.resume()

So that’s the quick & dirty way to call a REST API from Swift. There are a couple of gotchas though:
We’re assuming we’ll get results and they’ll be in the format we expect. This can cause definitely
cause problems, e.g., we’ll get a crash if our post isn’t a dictionary here:

post = try NSJSONSerialization.JSONObjectWithData(responseData,


options: []) as! NSDictionary

Either check the format to make sure it’s not nil and it’s actually a dictionary (which gets verbose
fast) or use SwiftyJSON⁶ to replace all that boilerplate:

// parse the result as JSON, since that's what the API provides
let post = JSON(data: responseData)
if let postID = post["id"].int {
print("The post ID is \(postID)")
}

SwiftyJSON will check for optionals at each step, so if post is nil or post["id"] is nil, then postID
will be nil. .int returns an optional like Int?. If you’re sure the value won’t be nil then use .intValue
instead to get a non optional value.
SwiftyJSON doesn’t just handle integers. Here’s how to parse strings, doubles and boolean values:

⁶https://github.com/SwiftyJSON/SwiftyJSON
Swift JSON Parsing & Networking Calls 101 22

let title = myJSON["title"].string


let cost = myJSON["cost"].double
let isPurchased = myJSON["purchased"].bool

If your JSON has an array of elements (e.g., a list of all of the posts) then you can get at each element
by index and access its properties in a single statement:

let thirdPostTitle = posts[3]["title"].string

So far the code to make the calls themselves is pretty verbose and the level of abstraction is low:
you’re thinking about posts but having to code in terms of HTTP requests and data tasks. Alamofire⁷
looks like a nice step up:

Alamofire.request(.GET, postEndpoint)
.responseJSON { response in
// get errors
print(response.result.error)
// get serialized data (i.e., JSON)
print(response.result.value)
// get raw data
print(response.data)
// get NSHTTPURLResponse
print(response.response)
}

Grab the code on GitHub: REST gists⁸

3.2 REST API Calls with Alamofire & SwiftyJSON


Last section we looked at the quick & dirty way to get access REST APIs in iOS. dataTaskWithRe-
quest works just fine for simple cases, like a URL shortener. But these days lots of apps have tons
of web service calls that are just begging for better handling: a higher level of abstraction, concise
syntax, simpler streaming, pause/resume, progress indicators, …
In Objective-C, this was a job for AFNetworking⁹. In Swift, Alamofire¹⁰ is our option for elegance.
While we’re harping on elegance, that JSON parsing was pretty ugly. Optional binding (as of Swift
1.2) helps fix that but SwiftyJSON¹¹ will really help clean up that boilerplate.
⁷https://github.com/Alamofire/Alamofire
⁸https://gist.github.com/cmoulton/01fdd4fe2c2e9c8195e1
⁹https://github.com/afnetworking/afnetworking
¹⁰https://github.com/Alamofire/Alamofire
¹¹https://github.com/SwiftyJSON/SwiftyJSON
Swift JSON Parsing & Networking Calls 101 23

Just like in previous section, we’ll use the super handy JSONPlaceholder¹² as our API.
Here’s our quick & dirty GET request from last section where we grabbed the first post and
printed out its title. (You’ll need to add import Foundation at the top of your file to have access
to NSJSONSerialization.):

let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"


guard let url = NSURL(string: postEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = NSURLRequest(URL: url)

let config = NSURLSessionConfiguration.defaultSessionConfiguration()


let session = NSURLSession(configuration: config)

let task = session.dataTaskWithRequest(urlRequest, completionHandler: { (data, response, e\


rror) in
guard let responseData = data else {
print("Error: did not receive data")
return
}
guard error == nil else {
print("error calling GET on /posts/1")
print(error)
return
}
// parse the result as JSON, since that's what the API provides
let post: NSDictionary
do {
post = try NSJSONSerialization.JSONObjectWithData(responseData,
options: []) as! NSDictionary
} catch {
print("error trying to convert data to JSON")
return
}
// now we have the post, let's just print it to prove we can access it
print("The post is: " + post.description)

// the post object is a dictionary


// so we just access the title using the "title" key
// so check for a title and print it if we have one
if let postTitle = post["title"] as? String {
print("The title is: " + postTitle)

¹²https://github.com/typicode/jsonplaceholder
Swift JSON Parsing & Networking Calls 101 24

}
})
task.resume()

Which is an awful lot of code for what we’re doing (but far less than back in the dark ages when
thousands of lines of code generated from WSDL web services would crash Xcode just by scrolling
the file). There’s no authentication and just enough error checking to get by.
Let’s see how this looks with Alamofire library that I keep talking up. First add Alamofire v3.1 to
your project using CocoaPods (See A Brief Introduction to CocoaPods if you’re not sure how). Then
set up the request:

let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"


Alamofire.request(.GET, postEndpoint)
.responseJSON { response in
// ...
}

Looks more readable to me so far. We’re telling Alamofire to set up & send an asynchronous request
to postEndpoint (without the ugly call to NSURL to wrap up the string). We explicitly say it’s a GET
request (instead of NSURLRequest assuming it). .GET is a member of the Alamofire.Method enum,
which also includes .POST, .PATCH, .OPTIONS, .DELETE, etc.
Then we get the data (asynchronously) as JSON in the .responseJSON. We could also use .response
(for an NSHTTPURLResponse), .responsePropertyList, or .responseString (for a string). We could
even chain multiple .responseX methods for debugging:

Alamofire.request(.GET, postEndpoint)
.responseJSON { response in
// handle JSON
}
.responseString{ response in
// print response as string for debugging, testing, etc.
print(response.result.value)
// check for errors
print(response.result.error)
}

That’s neat but right now we just want to get the post’s title from the JSON. We’ll make the request
then handle it with .responseJSON. Like last time we need to do some error checking:

1. Check for an error returned by the API call


2. If no error, see if we got any JSON results
3. Check for an error in the JSON transformation
4. If no error, access the post object in the JSON and print out the title

In SwiftyJSON, instead of
Swift JSON Parsing & Networking Calls 101 25

post["title"] as? String

we can use the cleaner

post["title"].string

It doesn’t make a huge difference if we’re unwrapping a single level but for multiple levels
unwrapping with nested if-lets would look like this:

if let postsArray = data as? NSArray {


if let firstPost = postsArray[0] as? NSDictionary {
if let title = firstPost["title"] as? String {
// ...
}
}
}

As of Swift 1.2 we can unwrap multiple optionals in a single if-let statement but it’s still kinda tough
to read:

if let postsArray = data as? NSArray,


firstPost = postsArray[0] as? NSDictionary,
title = firstPost["title"] as? String {
// ...
}

Compared to the following with SwiftyJSON:

if let title = postsArray[0]["title"].string


// ...
}

Ok, so all together now:


Swift JSON Parsing & Networking Calls 101 26

let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"


Alamofire.request(.GET, postEndpoint)
.responseJSON { response in
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error calling GET on /posts/1")
print(response.result.error!)
return
}

if let value: AnyObject = response.result.value {


// handle the results as JSON, without a bunch of nested if loops
let post = JSON(value)
// now we have the results, let's just print them
// though a table view would definitely be better UI:
print("The post is: " + post.description)
if let title = post["title"].string {
// to access a field:
print("The title is: " + title)
} else {
print("error parsing /posts/1")
}
}
}

So that’s a little cleaner: we check that the web service did return data then we use let post =
JSON(value) to create the post object. Much cleaner than the previous let post = NSJSONSeri-
alization.JSONObjectWithData(data, options: [], error: &jsonError) as! NSDictionary
(which crashed if the data wasn’t a dictionary).
To POST, we just need to change the HTTP method and provide the post data:

let postsEndpoint: String = "http://jsonplaceholder.typicode.com/posts"


let newPost = ["title": "Frist Psot", "body": "I iz fisrt", "userId": 1]
Alamofire.request(.POST, postsEndpoint, parameters: newPost, encoding: .JSON)
.responseJSON { response in
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error calling GET on /posts/1")
print(response.result.error!)
return
}

if let value: AnyObject = response.result.value {


// handle the results as JSON, without a bunch of nested if loops
Swift JSON Parsing & Networking Calls 101 27

let post = JSON(value)


print("The post is: " + post.description)
}
}

And DELETE is nice and compact:

let firstPostEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"


Alamofire.request(.DELETE, firstPostEndpoint)
.responseJSON { response in
if let error = response.result.error {
// got an error while deleting, need to handle it
print("error calling DELETE on /posts/1")
print(error)
}
}

Grab the example code on GitHub¹³.


So that’s one step better on our journey to nice, clean REST API calls. But we’re still interacting with
untyped JSON which can easily lead to errors. Next we’ll take another step towards a class for our
post objects using Alamofire with a custom response serializer.

3.3 Alamofire Router


Previously we set up some REST API Calls With Alamofire & SwiftyJSON. While it’s a bit of overkill
for those simple calls we can improve our code by using an Alamofire router. The router will compose
the URL requests for us which will avoid having URL strings throughout our code. A router can also
be used to apply headers, e.g., for including an OAuth token or other authorization header.
Using a router with Alamofire is good practice since it helps keep our code organized. The router
is responsible for creating the URL requests so that our API manager (or whatever makes the API
calls) doesn’t need to do that along with all of the other responsibilities that it has.
Our previous simple examples included calls to get, create, and delete posts using JSONPlaceholder¹⁴.
JSONPlaceholder is a fake online REST API for testing and prototyping. It’s like image placeholders
but for web developers.
Here’s our previous code:

¹³https://gist.github.com/cmoulton/01fdd4fe2c2e9c8195e1
¹⁴https://github.com/typicode/jsonplaceholder
Swift JSON Parsing & Networking Calls 101 28

// Get first post


let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
Alamofire.request(.GET, postEndpoint)
.responseJSON { response in
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error calling GET on /posts/1")
print(response.result.error!)
return
}

if let value: AnyObject = response.result.value {


// handle the results as JSON, without a bunch of nested if loops
let post = JSON(value)
// now we have the results, let's just print them
// though a table view would definitely be better UI:
print("The post is: " + post.description)
if let title = post["title"].string {
// to access a field:
print("The title is: " + title)
} else {
print("error parsing /posts/1")
}
}
}

// Create new post


let postsEndpoint: String = "http://jsonplaceholder.typicode.com/posts"
let newPost = ["title": "Frist Psot", "body": "I iz fisrt", "userId": 1]
Alamofire.request(.POST, postsEndpoint, parameters: newPost, encoding: .JSON)
.responseJSON { response in
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error calling GET on /posts/1")
print(response.result.error!)
return
}

if let value: AnyObject = response.result.value {


// handle the results as JSON, without a bunch of nested if loops
let post = JSON(value)
print("The post is: " + post.description)
}
}
Swift JSON Parsing & Networking Calls 101 29

// Delete first post


let firstPostEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
Alamofire.request(.DELETE, firstPostEndpoint)
.responseJSON { response in
if let error = response.result.error {
// got an error while deleting, need to handle it
print("error calling DELETE on /posts/1")
print(error)
}
}

The bits that we’ll be changing look like Alamofire.request(...). Currently we’re providing the
URL as a string, like http://jsonplaceholder.typicode.com/posts/1, and the HTTP method, like
.GET. Instead of these two parameters Alamofire.request(...) can also take a URLRequestCon-
vertible object such as an NSMutableURLRequest. That’s what we’ll take advantage of to create our
router.

3.3.1 Using an Alamofire Router


To start we’ll declare a router. It’ll be an enum with a case for each type of call we want to make. A
convenient feature of Swift enums is that the cases can have arguments. For example, our .Get case
can have an Int argument so we can pass in the ID number of the post that we want to get.
We’ll also need the base URL for our API. We can use a computed property to generate the
NSMutableURLRequest, which is another nice feature of Swift enums:

enum Router: URLRequestConvertible {


static let baseURLString = "http://jsonplaceholder.typicode.com/"

case Get(Int)
case Create([String: AnyObject])
case Delete(Int)

var URLRequest: NSMutableURLRequest {


...
// TODO: implement
}
}

We’ll come back and implement the URLRequest computed property in a bit. First let’s see how we
need to change our existing calls to use the router.
For the GET call all we need to do is to change:
Swift JSON Parsing & Networking Calls 101 30

Alamofire.request(.GET, postEndpoint)

to

Alamofire.request(Router.Get(1))

We can also delete this line since all of the URL string handling is now done within the Router:

let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"

The .POST call is similar. Change:

let postsEndpoint: String = "http://jsonplaceholder.typicode.com/posts"


let newPost = ["title": "Frist Psot", "body": "I iz fisrt", "userId": 1]
Alamofire.request(.POST, postsEndpoint, parameters: newPost, encoding: .JSON)

to

let newPost = ["title": "Frist Psot", "body": "I iz fisrt", "userId": 1]


Alamofire.request(Router.Create(newPost))

You can see there that the router has abstracted away the encoding as well as the endpoint from this
function.
And for the .DELETE call, change:

let firstPostEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"


Alamofire.request(.DELETE, firstPostEndpoint)

to

Alamofire.request(Router.Delete(1))

Now our calls are a bit easier to read. We could make them even clearer by naming the Router cases
more descriptively, like Router.DeletePostWithID(1).

3.3.2 Generating the URL Requests


The code in this section is pretty Swift-y so if it looks a little odd at first just keep reading.
Within the router we need a computed property so that our calls like Router.Delete(1) give us an
NSMutableURLRequest that Alamofire.Request() knows how to use.
We’ve defined the Router as an enum with a case for each of our 3 calls. So within our URLRequest
computed property we can use those 3 cases. For example, we can use a switch statement to define
the HTTP methods for each case:
Swift JSON Parsing & Networking Calls 101 31

var method: Alamofire.Method {


switch self {
case .Get:
return .GET
case .Create:
return .POST
case .Delete:
return .DELETE
}
}

And similarly we can create the NSMutableURLRequest in a switch statement:

let result: (path: String, parameters: [String: AnyObject]?) = {


switch self {
case .Get(let postNumber):
return ("posts/\(postNumber)", nil)
case .Create(let newPost):
return ("posts", newPost)
case .Delete(let postNumber):
return ("posts/\(postNumber)", nil)
}
}()

This switch statement has a few parameters (path: String, parameters: [String: AnyObject]?).
That’s the format of the output tuples it will return, like ("posts", newPost) for the .Create case.
It also uses the arguments for each case, like the number for the .Get case: case .Get(let
postNumber)\.

So we can put together those bits to generate the URL request. First generating an NSURL from the
base URL:

let URL = NSURL(string: Router.baseURLString)!

Appending the path components from the result switch statement:

let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(result.path))

Then creating a URL request including the encoded parameters: (encoding.encode(...) handles
nil parameters just fine so we don’t need to check for that):
Swift JSON Parsing & Networking Calls 101 32

let encoding = Alamofire.ParameterEncoding.JSON


let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)

Setting the HTTP method:

encodedRequest.HTTPMethod = method.rawValue

And finally returning the URL request:

return encodedRequest

All together:

var URLRequest: NSMutableURLRequest {


var method: Alamofire.Method {
switch self {
case .Get:
return .GET
case .Create:
return .POST
case .Delete:
return .DELETE
}
}

let result: (path: String, parameters: [String: AnyObject]?) = {


switch self {
case .Get(let postNumber):
return ("posts/\(postNumber)", nil)
case .Create(let newPost):
return ("posts", newPost)
case .Delete(let postNumber):
return ("posts/\(postNumber)", nil)
}
}()

let URL = NSURL(string: Router.baseURLString)!


let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(result.path))

let encoding = Alamofire.ParameterEncoding.JSON


let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)

encodedRequest.HTTPMethod = method.rawValue

return encodedRequest
}
Swift JSON Parsing & Networking Calls 101 33

Save and test out our code. The console log should display the same post titles and lack of errors
that we had in the previous section. We’ve created a simple Alamofire router that you can adapt to
your API calls.
Here’s the example code on GitHub¹⁵.

3.4 Strongly Typed GET and POST Calls with Alamofire


We’ve used Alamofire¹⁶ to make some REST requests to a web service. Now let’s clean that up by
building a higher layer of abstraction by mapping the JSON to a strongly typed class. That’ll keep
our code better organized so we aren’t struggling to keep too many details in our minds at once.
First, we’ll need a class to represent the Post objects we’re dealing with. Create a new class in its
own file to represent the Post objects. It will have a few properties, an initializer to create new Post
objects, and a description function to print out all of the properties, which is handy for debugging:

class Post {
var title:String?
var body:String?
var id:Int?
var userId:Int?

required init?(aTitle: String?, aBody: String?, anId: Int?, aUserId: Int?) {


self.title = aTitle
self.body = aBody
self.id = anId
self.userId = aUserId
}

func description() -> String {


return "ID: \(self.id)" +
"User ID: \(self.userId)" +
"Title: \(self.title)\n" +
"Body: \(self.body)\n"
}
}

We’ll be using our router to handle creating the URL requests. It assembles the requests including the
HTTP method and the URL, plus any parameters or headers. We don’t need to make any changes
since the router still works in terms of url requests and JSON. It doesn’t need to know anything
about our Post objects.
Create a new file PostRouter.swift for our router:
¹⁵https://github.com/cmoulton/grokRouter
¹⁶https://github.com/Alamofire/Alamofire
Swift JSON Parsing & Networking Calls 101 34

import Alamofire

enum PostRouter: URLRequestConvertible {


static let baseURLString = "http://jsonplaceholder.typicode.com/"

case Get(Int)
case Create([String: AnyObject])
case Delete(Int)

var URLRequest: NSMutableURLRequest {


var method: Alamofire.Method {
switch self {
case .Get:
return .GET
case .Create:
return .POST
case .Delete:
return .DELETE
}
}

let result: (path: String, parameters: [String: AnyObject]?) = {


switch self {
case .Get(let postNumber):
return ("posts/\(postNumber)", nil)
case .Create(let newPost):
return ("posts", newPost)
case .Delete(let postNumber):
return ("posts/\(postNumber)", nil)
}
}()

let URL = NSURL(string: PostRouter.baseURLString)!


let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(result.path))

let encoding = Alamofire.ParameterEncoding.JSON


let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)

encodedRequest.HTTPMethod = method.rawValue

return encodedRequest
}
}

When setting up API calls I like to work backwards. Starting with the calls we’d like to make then
Swift JSON Parsing & Networking Calls 101 35

figuring out how to make them work. First, we want to be able to GET a post from an ID number.
We can do this in the View Controller’s viewWillAppear function:

override func viewWillAppear(animated: Bool) {


super.viewWillAppear(animated)

// MARK: Get Post #1


Post.postByID(1, completionHandler: { result in
if let error = result.error {
// got an error in getting the data, need to handle it
print("error calling POST on /posts")
print(error)
return
}
guard let post = result.value else {
print("error calling POST on /posts: result is nil")
return
}
// success!
print(post.description())
print(post.title)
})
}

postByID will take a completion handler. Unlike previous code that we’ve written, we’re using a
completion handler in a function that we’ve written/ We’re not just providing a completion handler
to one of Apple or Alamofire’s functions. We’ll see how that works when we implement postByID
and how we’ll call the completion handler when we want this function to deal with the results.
We’re using a completion handler so we can make the API calls asynchronously. Notice that there
are no references to URLs or requests or JSON in the code above. It deals entirely with Posts, not the
underlying levels of abstraction.
We’ll also want to be able to create Posts by sending them to the server. We’re using the trailing
block syntax for newPost.save so we dropped the completionHandler: label from the function call:
Swift JSON Parsing & Networking Calls 101 36

// MARK: POST
// Create new post
guard let newPost =
Post(aTitle: "Frist Psot", aBody: "I iz fisrt", anId: nil, aUserId: 1) else {
print("error: newPost isn't a Post")
return
}
newPost.save { result in
if let error = result.error {
// got an error in getting the data, need to handle it
print("error calling POST on /posts")
print(error)
return
}
guard let post = result.value else {
print("error calling POST on /posts: result is nil")
return
}
// success!
print(post.description())
print(post.title)
}

We’ve separated creating a new Post object locally from saving it on the server (Post(...) vs
newPost.save(...)). We’re leaving the ID number blank on creation since that will be assigned
by the server.
Let’s set up some Alamofire requests and see how we can interface them to those Post calls. First
the GET request (using our handy-dandy router that created URL requests for us):

Alamofire.request(PostRouter.Get(1))
.responseJSON { response in
// ...
}

Wouldn’t it be nice if we could use .responseObject to get a Post object instead of .responseJSON?
Turns out we can: Alamofire lets us define custom response serializers that we can use to turn the
results of our API call to whatever that we want. A handy-dandy serializer that you’ll want to keep
around is for returning a single object from JSON.
A response serializer takes the results of the URL request and turns them in to a form that we want
to work with. By default, the URL request gets the results as NSData but we’d rather work with a
more convenient format like JSON or an object.
To create that serializer, we’ll have to extend Alamofire.Request. We’ll create a new file for it called
AlamofireRequest+JSONSerializable.swift. Here’s what it’ll look like, we’ll step through how it
works shortly:
Swift JSON Parsing & Networking Calls 101 37

import Foundation
import Alamofire
import SwiftyJSON

extension Alamofire.Request {
public func responseObject<T: ResponseJSONObjectSerializable>(completionHandler:
Response<T, NSError> -> Void) -> Self {
let serializer = ResponseSerializer<T, NSError> { request, response, data, error in
guard error == nil else {
return .Failure(error!)
}
guard let responseData = data else {
let failureReason = "Object could not be serialized because input data was nil."
let error = Error.errorWithCode(.DataSerializationFailed, failureReason:
failureReason)
return .Failure(error)
}

let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)


let result = JSONResponseSerializer.serializeResponse(request, response,
responseData, error)

switch result {
case .Success(let value):
let json = SwiftyJSON.JSON(value)
if let object = T(json: json) {
return .Success(object)
} else {
let failureReason = "Object could not be created from JSON."
let error = Error.errorWithCode(.JSONSerializationFailed, failureReason:
failureReason)
return .Failure(error)
}
case .Failure(let error):
return .Failure(error)
}
}

return response(responseSerializer: serializer, completionHandler: completionHandler)


}
}

responseObject<...>(...) declares a new .responseX handling function on Alamofire.Request.


The only difference from the standard response function is that it’s using our new responseSeri-
alizer which is a custom response serializer. So we’ll be able to call it like:
Swift JSON Parsing & Networking Calls 101 38

Alamofire.request(PostRouter.Get(id))
.responseObject { (response: Response<Post, NSError>) in
// Do stuff with post
}

We’ll walk through the responseObject function one layer at a time. Starting with the function
declaration:

extension Alamofire.Request {
public func responseObject<T: ResponseJSONObjectSerializable>(completionHandler:
Response<T, NSError> -> Void) -> Self {
...
}
}

It defines a function called responseObject. The <T> bit means this is a generic method: it can work
with different types of objects. <T: ResponseJSONObjectSerializable> means that those types
must implement the ResponseJSONObjectSerializable protocol (which we’ll have to define). We
need that protocol so we can guarantee that any type of object that we pass in will have an init
function that takes JSON.
The responseObject function takes a single argument called completionHandler. As you might
guess, that’s the method we’ll call when we’re done parsing the JSON and creating the object (i.e.,
to handle the completion of this function). It’s used so we can call this method asynchronously: the
caller doesn’t need to wait around for a response, it’ll get notified when we’ve got results for it.
The completion handler has a single argument: Response<T, NSError>. Alamofire 3 defines the
Response struct. It’s a handy way to pack up a bunch of bits where we used to have to use a big
tuple like (NSURLRequest?, NSHTTPURLResponse?, Result<T, NSError>), with the Result struct
packing up the result (our T object and/or an error).
Think of the Response and Result structs as little packages of data that make up what we get from
fetching the response and serializing it into the format that we want. It’s kind of like when you buy
something in person. You hand over your payment and you get back a few things: your purchase,
your change, a receipt, or maybe an error message like “card declined” or “you’re short 8 cents”. All
of these things make up the response to your purchase.
You could also think of the purchase and/or error message as the result of your transaction, whether
it’s a success or a failure.
Alamofire’s structs are similar: Result has .Success and .Failure cases and might have what you
asked for or an error. Response is higher level: it packs up the Result along with all of the other info
from the transaction like your original request and the raw response.
And the responseObject function returns an Alamofire.Request object. -> Self specifies the return
type.
Now, we can look at the structure of the responseObject function:
Swift JSON Parsing & Networking Calls 101 39

public func responseObject<T: ResponseJSONObjectSerializable>(completionHandler:


Response<T, NSError> -> Void) -> Self {
let serializer = ResponseSerializer<T, NSError> { (request, response, data, error) in
// ...
}

return response(responseSerializer: serializer, completionHandler: completionHandler)


}

Within responseObject we create a response serializer that will work with our generic T type and
an NSError. The serializer will take in the results of the URL request (request, response, data,
error) and use the Result type defined by Alamofire to return success (with the object) or failure
(with an error). Our responseObject function just returns the responseSerializer that gets created
and allows passing the completion handler where it needs to go.
And finally we can look at how we’ll implement responseSerializer:

let serializer = ResponseSerializer<T, NSError> { request, response, data, error in


guard error == nil else {
return .Failure(error!)
}
guard let responseData = data else {
let failureReason = "Object could not be serialized because input data was nil."
let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
return .Failure(error)
}

let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)


let result = JSONResponseSerializer.serializeResponse(request, response,
responseData, error)

switch result {
case .Success(let value):
let json = SwiftyJSON.JSON(value)
if let object = T(json: json) {
return .Success(object)
} else {
let failureReason = "Object could not be created from JSON."
let error = Error.errorWithCode(.JSONSerializationFailed, failureReason:
failureReason)
return .Failure(error)
}
case .Failure(let error):
return .Failure(error)
}
}
Swift JSON Parsing & Networking Calls 101 40

The custom response serializer first checks that it has valid data using guard. Then it turns the data
in to JSON and parses it using SwiftyJSON. Then it creates a new copy of the type of class we called
it with from the JSON:

let newObject = T(json: json)

If we can’t create the object from the JSON we return a relevant error.
In order for that serializer to work we need to set up the ResponseJSONObjectSerializable protocol:

public protocol ResponseJSONObjectSerializable {


init?(json: SwiftyJSON.JSON)
}

Which just defines that conforming types need to have an init method that works with JSON. It’s
what tells our generic objects what they need to be able to do. In this case, they just need to be able
to be created from JSON. Let’s implement that for our Post class:

final class Post: ResponseJSONObjectSerializable {


var title:String?
var body:String?
var id:Int?
var userId:Int?

...

required init?(json: SwiftyJSON.JSON) {


self.title = json["title"].string
self.body = json["body"].string
self.id = json["id"].int
self.userId = json["userId"].int
}

...
}

Using SwiftyJSON it’s easy to parse out the contents of the JSON into the properties of the Post
object.
Now we need to mate up the Post.postById() call to the GET call using the custom serializer:
Swift JSON Parsing & Networking Calls 101 41

class Post {
...

// MARK: API Calls


class func postByID(id: Int, completionHandler: (Result<Post, NSError>) -> Void) {
Alamofire.request(PostRouter.Get(id))
.responseObject { (response: Response<Post, NSError>) in
completionHandler(response.result)
}
}
}

And that’s it for the GET call. We can run our nice pretty Post.postByID(1) call now.
But, of course, there are always more requirements. We said we’d implement the POST call to save
new Posts too.
In this case, there isn’t a special class in Alamofire to customize. We just have to get the Post into
the correct format to send to the API.
In our Post class, we’ll need a method to turn a Post into a Dictionary with String keys (which we’ll
call json for convenience):

func toJSON() -> Dictionary<String, AnyObject> {


var json = Dictionary<String, AnyObject>()
if let title = title {
json["title"] = title
}
if let body = body {
json["body"] = body
}
if let id = id {
json["id"] = id
}
if let userId = userId {
json["userId"] = userId
}
return json
}

We’re using Dictionary instead of NSDictionary because that’s what an Alamofire.Request takes.
To finish implementing the save() function for Posts:
Swift JSON Parsing & Networking Calls 101 42

// POST / Create
func save(completionHandler: (Result<Post, NSError>) -> Void) {
guard let fields:Dictionary<String, AnyObject> = self.toJSON() else {
print("error: error converting newPost fields to JSON")
return
}
Alamofire.request(PostRouter.Create(fields))
.responseObject { (response: Response<Post, NSError>) in
completionHandler(response.result)
}
}

3.5 And That’s All


And that’s it! Now we can run our nice pretty calls to retrieve and save posts that we set up at
the start. Even better, the caller of those functions no longer has any knowledge of how the posts
are getting retrieved & saved. We could completely remove Alamofire to switch to RESTKit¹⁷ or a
completely different implementation¹⁸ for the API calls without having to touch the view controller
code at all.
Here’s the demo code for strongly typed API calls with an Alamofire Router¹⁹.

Don’t worry if you have more complex JSON to parse. Start with some simple String,
numeric and boolean fields for now. Later we’ll do some more complex JSON parsing like
handling arrays and dates. Set up one more two simple API calls with custom response
serializers or reuse the generic serializers from this chapter.

In the following chapters we’ll build out our gists app using Alamofire. We’ll set up the API calls
that we need and tie the results into our user interface. Our UI will include a table view, transitions
to detail views for individual gists, a form to create new gists, pull to refresh and swipe to delete.
Then we’ll discuss what we can do if we don’t have an internet connection.
¹⁷https://github.com/RestKit/RestKit
¹⁸http://en.wikipedia.org/wiki/IP_over_Avian_Carriers
¹⁹https://github.com/cmoulton/grokRouterAndStrongTypes
4. Why I Use Libraries Like Alamofire
There are a bunch of jokes about that 2 hardest things in programming. Some say it’s naming things,
estimating and off-by-one errors. Some say it’s estimating & getting paid. I think it’s nailing down
your requirements so you know what needs to get done and keeping your code at a single level of
abstraction.
What do I mean by a single level of abstraction? Well, consider some crufty old Objective-C code:

NSArray *myGists = [[NSArray alloc] initWithObjects:


[NSString stringWithString:@"text of gist 1"],
[NSString stringWithString:@"text of gist 2"],
nil];

// do stuff with myGists

[myArray release];

That code does something with an array of Gists. But instead of being able to just think about gists,
the programmer working on this code also has to think about managing the memory that holds
those gists (in alloc and release). So they’ve got to keep 2 different levels of abstraction in their
mind. Those objects aren’t just gists to them, they’re also hunks of memory.
Sure, under it all the gists are really hunks of memory. And there’s got to be code somewhere that’s
aware of that. But it doesn’t have to be in the same place as gist-y operations like starring a gist or
editing the text. And that’s how it is with code to hook in to web services:

• Somewhere your code has to know about the low level network stuff
• Somewhere it has to handle JSON
• Somewhere it has to work with gists (or whatever your model objects are)

Those three levels of abstraction don’t need to (and shouldn’t) overlap. It’s much less exhausting to
work on code at a single level without having to keep flipping your understanding of the code up &
down to higher and lower abstractions.
You don’t need to use libraries like SwiftyJSON and Alamofire. But they’re often a really good
wrapper around lower levels of abstraction. And if they’re open source so you can tweak the code
later if you need to, what have you go to lose?

43
5. Hooking Up a REST API to a Table
View
The UITableView is the bread & butter of tons of iOS apps. Combined with access to a web service,
it’s the core of apps from Mailbox to Twitter to Facebook, even Apple’s Notes & App Store apps.
We’re going to set up an Xcode project that will get data from the GitHub gists API. We’ll set up
a table view that displays the public gists. We’ll need to make a GET API call, parse the returned
JSON, and set up a table view to display the results.

This chapter doesn’t explain the basics of adding a UITableView to a Swift app. If you’re fuzzy
on the details of implementing a UITableView, take a read through Apple’s docs or a nice quick
tutorial. This chapter is all about tying a table view to an API call returning an array of items.
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/
CreateConfigureTableView/CreateConfigureTableView.html
https://www.weheartswift.com/how-to-make-a-simple-table-view-with-ios-8-and-swift/

If you don’t want to type along, grab the code from GitHub (tagged “tableview”)¹.

5.1 Our Swift Project


We’re going to start working on our GitHub Gists app now, so first we need to create a project in
Xcode:
Fire up Xcode.
Create a new master-detail Swift project (make it universal if you want to). Make sure you select
Swift for the language and don’t include Core Data.
Add Alamofire 3.1² & SwiftyJSON 2.3³ using CocoaPods (not sure how? Check out the appendix).
Open the .xcworkspace file.
We’re not going to use most of the boilerplate that Xcode has generated for now, so just ignore it
until later. Notice that Xcode created 2 view controllers: a MasterViewController that’s a table view
¹https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/tableview
²https://github.com/Alamofire/Alamofire
³https://github.com/SwiftyJSON/SwiftyJSON

44
Hooking Up a REST API to a Table View 45

controller and a detailViewController. Those will work nicely for our list of gists and detailed gist
view. We’ll mostly be working with the MasterViewController for the next few chapters.
Create a new file: GitHubAPIManager.swift. This class with be responsible for our API interactions.
It’ll help us keep our code organized so our view controllers don’t end up as monstrously huge files.
We’ll also be able to more easily share code between the different view controllers.
At the top of the new file, import Alamofire and SwiftyJSON:

import Foundation
import Alamofire
import SwiftyJSON

class GitHubAPIManager {

If you’re working with a different API you probably want to name this file something more
relevant than GitHubAPIManager.

While working with an API you’ll often end up with a bunch of code that isn’t specific to an object.
You might have to set custom headers, keep track of OAuth tokens, handle secrets & IDs, and handle
authorization or other general errors. To keep this code from being spread out in a bunch of places
like the App Delegate and our model objects, we’ll use our GitHubAPIManager class to handle it.
Since there’s only one GitHub API that we’re interacting with, it makes sense to only have a single
API manager in our app. So let’s set up this class to have a sharedInstance that we’ll access to get
our single GitHubAPIManager object:

import Foundation
import Alamofire
import SwiftyJSON

class GitHubAPIManager {
static let sharedInstance = GitHubAPIManager()
}

Now we can start setting up the API call to get the public gists which doesn’t require authentication.
To keep things simple while we get the API call working we’ll just print out the results now. Then
we’ll integrate them with the table view.
So let’s declare our simple method:
Hooking Up a REST API to a Table View 46

class GitHubAPIManager {
...

func printPublicGists() -> Void {


// TODO: implement
}
}

While we’re at it, let’s create a Router too. In GistRouter.swift. The router will be responsible for
creating the URL requests and stop our API manager from getting unweildy. It’s pretty similar to
the one we set up in the 101 chapter except we’re starting with just one case to make a GET call for
public gists:

import Foundation
import Alamofire

enum GistRouter: URLRequestConvertible {


static let baseURLString:String = "https://api.github.com"

case GetPublic() // GET https://api.github.com/gists/public

var URLRequest: NSMutableURLRequest {


var method: Alamofire.Method {
switch self {
case .GetPublic:
return .GET
}
}

let result: (path: String, parameters: [String: AnyObject]?) = {


switch self {
case .GetPublic:
return ("/gists/public", nil)
}
}()

let URL = NSURL(string: GistRouter.baseURLString)!


let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))

let encoding = Alamofire.ParameterEncoding.JSON


let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)

encodedRequest.HTTPMethod = method.rawValue

return encodedRequest
Hooking Up a REST API to a Table View 47

}
}

Then to get the public gists:

func printPublicGists() -> Void {


Alamofire.request(GistRouter.GetPublic())
.responseString { response in
if let receivedString = response.result.value {
print(receivedString)
}
}
}

To test this code out, go to the MasterViewController and add a viewDidAppear function. It’ll get
run each time the main view is shown, like right after launch:

override func viewDidAppear(animated: Bool) {


super.viewDidAppear(animated)
// TEST
GitHubAPIManager.sharedInstance.printPublicGists()
// END TEST
}

Now save & run your project. You should see an empty table view in the simulator or on your
iPhone. But if your API call succeeds then at the bottom of the screen (in the console) you should
see a print out of a bunch of JSON:

"[{\"url\":\"https://api.github.com/gists/35877917945abf44fc7a\",\"forks_url\":\"https://a\
pi.github.com/gists/35877917945abf44fc7a/forks\",\"commits_url\":\"https://api.github.com/\
gists/35877917945abf44fc7a/commits\",\"id\":\"35877917945abf44fc7a\",\"git_pull_url\":\"ht\
tps://gist.github.com/35877917945abf44fc7a.git\",\"git_push_url\":\"https://gist.github.co\
m/35877917945abf44fc7a.git\",\"html_url\":\ ...

Add an API calling function like printPublicGists to your API manager. It should retrieve
an array of objects and print them to the console.

5.2 Analyzing the API JSON Response


The API call we’ll be using to get a list of gists will return a great big hunk of JSON containing an
array of gists. When we check the API docs for gists⁴ we can see that the JSON representing a gist
has tons of data, mostly about the gists’s owner, the files it contains, and its history:
⁴https://developer.github.com/v3/gists/
Hooking Up a REST API to a Table View 48

{
"url": "https://api.github.com/gists/aa5a315d61ae9438b18d",
"forks_url": "https://api.github.com/gists/aa5a315d61ae9438b18d/forks",
"commits_url": "https://api.github.com/gists/aa5a315d61ae9438b18d/commits",
"id": "aa5a315d61ae9438b18d",
"description": "description of gist",
"public": true,
"owner": {
"login": "octocat",
"id": 1,
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"user": null,
"files": {
"ring.erl": {
"size": 932,
"raw_url": "https://gist.githubusercontent.com/raw/365370/8c4d2d43d178df44f4c03a7f2a\
c0ff512853564e/ring.erl",
"type": "text/plain",
"language": "Erlang",
"truncated": false,
"content": "contents of gist"
}
},
"comments": 0,
"comments_url": "https://api.github.com/gists/aa5a315d61ae9438b18d/comments/",
"html_url": "https://gist.github.com/aa5a315d61ae9438b18d",
"git_pull_url": "https://gist.github.com/aa5a315d61ae9438b18d.git",
"git_push_url": "https://gist.github.com/aa5a315d61ae9438b18d.git",
"created_at": "2010-04-14T02:15:15Z",
"updated_at": "2011-06-20T11:34:15Z",
Hooking Up a REST API to a Table View 49

"forks": [
{
"user": {
"login": "octocat",
"id": 1,
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"url": "https://api.github.com/gists/dee9c42e4998ce2ea439",
"id": "dee9c42e4998ce2ea439",
"created_at": "2011-04-14T16:00:49Z",
"updated_at": "2011-04-14T16:00:49Z"
}
],
"history": [
{
"url": "https://api.github.com/gists/aa5a315d61ae9438b18d/57a7f021a713b1c5a6a199b54c\
c514735d2d462f",
"version": "57a7f021a713b1c5a6a199b54cc514735d2d462f",
"user": {
"login": "octocat",
"id": 1,
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
Hooking Up a REST API to a Table View 50

"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"change_status": {
"deletions": 0,
"additions": 180,
"total": 180
},
"committed_at": "2010-04-14T02:15:15Z"
}
]
}

We’ll convert that JSON into Swift objects. We’ll create a new Gist class that will represent the gists
in our code. So add a new Swift file to your Xcode project and name it Gist. In that file, define a
Gist class:

import Foundation

class Gist {

Look at your API and create a model object class to represent the objects you’ll want to
display in your table view.

Now we need to decide what bits of the JSON we want. We could extract all of that info but since
we aren’t going to display all of it that would be a lot of work for nothing. We can always come
back and parse more fields later.
What do we want to display? Well, we’ll have a table view cell with a title, subtitle and image so we
need to fill those in. Let’s use the gist’s description, the author’s GitHub ID and the author’s avatar.
It might also be handy to have the unique ID for each gist and the url where it’s located. So we need
to parse those from the JSON for each gist and use them to create a new gist object. First add those
items as properties to the Gist class:
Hooking Up a REST API to a Table View 51

class Gist {
var id: String?
var description: String?
var ownerLogin: String?
var ownerAvatarURL: String?
var url: String?
}

For your model object determine which properties you need to extract from the JSON for
display. Create a model class like Gist above with your properties.

We’ll want to create new gists from JSON so we can add an initializer for that class that takes in
JSON to create a gist object. We’ll need to import SwiftyJSON. We’ll also create a simple init function
that we can use to create Gists without pulling them down from the GitHub API:

import SwiftyJSON

class Gist {
var id: String?
var description: String?
var ownerLogin: String?
var ownerAvatarURL: String?
var url: String?

required init(json: JSON) {


self.description = json["description"].string
self.id = json["id"].string
self.ownerLogin = json["owner"]["login"].string
self.ownerAvatarURL = json["owner"]["avatar_url"].string
self.url = json["url"].string
}

required init() {
}
}

Set up your init function to take one of the entries in your JSON and create an instance
of your model object class. If some of your properties aren’t strings then refer to the 101
chapter to parse numbers and boolean values. If you have arrays of items (like the Files for
a gist) or dates we’ll cover those when we create a detail view.
Hooking Up a REST API to a Table View 52

5.3 Setting Up the Table View


Now, we can get into the code. Since we let Xcode create a Master-Detail project it has filled in
a bunch of code for us. Let’s quickly step through the MasterViewController so we know what’s
already done for us. Starting at the top:

class MasterViewController: UITableViewController {

var detailViewController: DetailViewController? = nil


var objects = [AnyObject]()

There’s a MasterViewController with a connection to a DetailViewController (to show a detail


view when we tap on a row). And there’s an array of objects. We’ll want to change those to gists
and declare them as an array of Gists so we know what we’re working with:

class MasterViewController: UITableViewController {

var detailViewController: DetailViewController? = nil


var gists = [Gist]()

Name that array and the model object class so they’re relevant to your app.

Next:

override func viewDidLoad() {


super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.navigationItem.leftBarButtonItem = self.editButtonItem()

let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self,


action: "insertNewObject:")
self.navigationItem.rightBarButtonItem = addButton
if let split = self.splitViewController {
let controllers = split.viewControllers
self.detailViewController = (controllers[controllers.count-1] as!
UINavigationController).topViewController as? DetailViewController
}
}
Hooking Up a REST API to a Table View 53

In viewDidLoad a few buttons get added to the navigation bar: an edit button on the left and an add
button on the right.
A connection to the detailViewController gets hooked up so we can use it later tell the
detailViewController to display the details for the selected gist.

Then:

override func viewWillAppear(animated: Bool) {


self.clearsSelectionOnViewWillAppear = self.splitViewController!.collapsed
super.viewWillAppear(animated)
}

Just before the view appears there’s some tweaking to a display setting: clearsSelectionOn-
ViewWillAppear means that rows can stay selected even if we go to another screen. That makes
sense for the iPad split view but not for the iPhone view that only shows the table view.
We’ll need to load the data when this view gets shown, so let’s add that now. We’ll do it in the
viewDidAppear function:

func loadGists() {
GitHubAPIManager.sharedInstance.printPublicGists()
}

override func viewDidAppear(animated: Bool) {


super.viewDidAppear(animated)
loadGists()
}

We’d normally do data loading in viewWillAppear so it’s on the screen as soon as possible. But
later we’re going to want to pop up a login view if they haven’t logged in already and we can’t
present a new view controller until the current view controller has finished appearing. So we’re
using viewDidAppear.
.

Set up a function like loadGists to retrieve your model objects.

Later we’ll need to replace the call to printPublicGists in loadGists with a function that gets us
the array of Gists in this class so we can display them.
Hooking Up a REST API to a Table View 54

override func didReceiveMemoryWarning() {


super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

If we have significant assets (like large images) or anything that can easily be recreated then
getting rid of them in didReceiveMemoryWarning lets our app handle warnings about low memory
gracefully.

func insertNewObject(sender: AnyObject) {


objects.insert(NSDate(), atIndex: 0)
let indexPath = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
}

insertNewObject was hooked up to the add button in viewDidLoad. It creates a new object and adds
it to the table view. It’s going to be a while before we get around to implementing creating a new
gist so let’s just add an alert about this button not working yet:

func insertNewObject(sender: AnyObject) {


let alert = UIAlertController(title: "Not Implemented", message:
"Can't create new gists yet, will implement later",
preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default,
handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}

Next we have prepareForSegue which sets up for a transition to the detail view:

// MARK: - Segues

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {


if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let object = objects[indexPath.row] as! NSDate
let controller =
(segue.destinationViewController as!
UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem =
self.splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true
Hooking Up a REST API to a Table View 55

}
}
}

Again we need to convert it to use our gists instead of generic objects. We’ll also have it check that
the destination is a DetailViewController instead of just assuming that’s what we’re getting:

// MARK: - Segues

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {


if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let gist = gists[indexPath.row] as Gist
if let detailViewController = (segue.destinationViewController as!
UINavigationController).topViewController as?
DetailViewController {
detailViewController.detailItem = gist
detailViewController.navigationItem.leftBarButtonItem =
self.splitViewController?.displayModeButtonItem()
detailViewController.navigationItem.leftItemsSupplementBackButton = true
}
}
}
}

Later we’ll set up the detail view controller to display gists.


Next come the methods for telling the table view what to display:

// MARK: - Table View

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {


return 1
}

override func tableView(tableView: UITableView,


numberOfRowsInSection section: Int) -> Int {
return objects.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath:


NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
Hooking Up a REST API to a Table View 56

let object = objects[indexPath.row] as! NSDate


cell.textLabel!.text = object.description
return cell
}

Again, we need to switch to gists and change tableView:cellForRowAtIndexPath:indexPath: to


use our new properties to display the description and owner ID. We’ll come back and set the owner’s
avatar image later since that’s a bit of work and we don’t want to get off track.
First make an adjustment to the table view cells in the storyboard so they have 2 lines of text in
them:

1. Open the main storyboard and select the table view in the master view controller
2. Select the prototype table view cell and change its style to Subtitle so we can have 2 lines of
text in it

Select table view


Hooking Up a REST API to a Table View 57

Change cell style to subtitle

Then we can update the code to show our gists:

// MARK: - Table View

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {


return 1
}

override func tableView(tableView: UITableView,


numberOfRowsInSection section: Int) -> Int {
return gists.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath


indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)

let gist = gists[indexPath.row]


cell.textLabel!.text = gist.description
cell.detailTextLabel!.text = gist.ownerLogin
// TODO: set cell.imageView to display image at gist.ownerAvatarURL

return cell
}

The next bit refers to editing the gists: deleting and creating them. We’ll just tweak it to not allow
editing for now:
Hooking Up a REST API to a Table View 58

override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath:


NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}

override func tableView(tableView: UITableView, commitEditingStyle editingStyle:


UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
objects.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
} else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array,
// and add a new row to the table view.
}
}

Becomes:

override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath:


NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return false
}

override func tableView(tableView: UITableView, commitEditingStyle editingStyle:


UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
gists.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
} else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array,
// and add a new row to the table view.
}
}

You should now be able to run the app without errors, though it’ll just display an empty table view.
To test our table view code we can create a few fake local Gists instead of loading them from GitHub.
To do so change loadGists() to create a few gists in the gists array:
Hooking Up a REST API to a Table View 59

func loadGists() {
let gist1 = Gist()
gist1.description = "The first gist"
gist1.ownerLogin = "gist1Owner"
let gist2 = Gist()
gist2.description = "The second gist"
gist2.ownerLogin = "gist2Owner"
let gist3 = Gist()
gist3.description = "The third gist"
gist3.ownerLogin = "gist3Owner"
gists = [gist1, gist2, gist3]

// Tell the table view to reload


self.tableView.reloadData()
}

Save & run to make sure that works. It should look like this:

Showing fake gists

And you should get an alert when you tap the add button:
Hooking Up a REST API to a Table View 60

Not yet implemented error

Follow through the previous section to make sure that you can create your model objects
and display them in the table view.

Once you’re sure that works, restore the loadGists() to the way it was:

func loadGists() {
GitHubAPIManager.sharedInstance.printPublicGists()
}

5.4 Getting & Processing the API Response


Back in the 101 chapter we created a handy extension to Alamofire.Request

public func responseObject<T: ResponseJSONObjectSerializable>

That extension let us handle a response from Alamofire that returned an object as JSON and
create a Swift object (as long as the Swift class had the right kind of initializer, as per the
ResponseJSONObjectSerializable protocol). Now we need something similar but for a whole array
of objects: We want to take a JSON array and turn it into an array of Swift objects. We’ll keep our
protocol so we need to add to our project. Add a new ResponseJSONObjectSerializable.swift file
and add the protocol to it. You’ll need to import SwiftyJSON:
Hooking Up a REST API to a Table View 61

import Foundation
import SwiftyJSON

public protocol ResponseJSONObjectSerializable {


init?(json: SwiftyJSON.JSON)
}

And tell the compiler that our Gist class implements that protocol (which it already does since it
has an initializer that takes JSON):

class Gist: ResponseJSONObjectSerializable {


...
}

We’ll also copy over the responseObject function since we might want it later. We’ll create
a new file for it named AlamofireRequest+JSONSerializable.swift, since it’s an extension to
Alamofire.Request that adds JSON serialization:

public func responseObject<T: ResponseJSONObjectSerializable>(completionHandler:


Response<T, NSError> -> Void) -> Self {
let serializer = ResponseSerializer<T, NSError> { request, response, data, error in
guard error == nil else {
return .Failure(error!)
}
guard let responseData = data else {
let failureReason = "Object could not be serialized because input data was nil."
let error = Error.errorWithCode(.DataSerializationFailed,
failureReason: failureReason)
return .Failure(error)
}

let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)


let result = JSONResponseSerializer.serializeResponse(request, response,
responseData, error)

switch result {
case .Success(let value):
let json = SwiftyJSON.JSON(value)
if let object = T(json: json) {
return .Success(object)
} else {
let failureReason = "Object could not be created from JSON."
let error = Error.errorWithCode(.JSONSerializationFailed,
failureReason: failureReason)
Hooking Up a REST API to a Table View 62

return .Failure(error)
}
case .Failure(let error):
return .Failure(error)
}
}

return response(responseSerializer: serializer, completionHandler: completionHandler)


}

Now we need something similar but for an array so it returns an array of objects: [T] instead of just
an object: T:

extension Alamofire.Request {
public func responseObject<T: ResponseJSONObjectSerializable>(completionHandler:
Response<T, NSError> -> Void) -> Self {
let serializer = ResponseSerializer<T, NSError> {
// ...
}

return response(responseSerializer: serializer, completionHandler: completionHandler)


}

public func responseArray<T: ResponseJSONObjectSerializable>(completionHandler:


Response<[T], NSError> -> Void) -> Self {
let serializer = ResponseSerializer<[T], NSError> {
// ...
}

return response(responseSerializer: serializer, completionHandler: completionHandler)


}
}

Which can be implemented pretty similarly:


Hooking Up a REST API to a Table View 63

public func responseArray<T: ResponseJSONObjectSerializable>(


completionHandler: Response<[T], NSError> -> Void) -> Self {
let serializer = ResponseSerializer<[T], NSError> { request, response, data, error in
guard error == nil else {
return .Failure(error!)
}
guard let responseData = data else {
let failureReason = "Array could not be serialized because input data was nil."
let error = Error.errorWithCode(.DataSerializationFailed,
failureReason: failureReason)
return .Failure(error)
}

let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)


let result = JSONResponseSerializer.serializeResponse(request, response,
responseData, error)

switch result {
case .Success(let value):
let json = SwiftyJSON.JSON(value)
var objects: [T] = []
for (_, item) in json {
if let object = T(json: item) {
objects.append(object)
}
}
return .Success(objects)
case .Failure(let error):
return .Failure(error)
}
}

return response(responseSerializer: serializer, completionHandler: completionHandler)


}

The big difference is that we iterate through the elements in the json: for (_, item) in json and
create an object out of each one: let object = T(json: item), adding them to the array if the
creation succeeds.
Now we need to:

1. Set up a function that GETs the public gists, parses them into an array and returns them
2. Hook up that function and its returned array into the table view

The getPublicGists function will look a lot like our printPublicGists function:
Hooking Up a REST API to a Table View 64

func printPublicGists() -> Void {


Alamofire.request(GistRouter.GetPublic())
.responseString { response in
if let receivedString = response.result.value {
print(receivedString)
}
}
}

The big difference is that it needs to return the array instead of printing it. So we’ll replace
responseString with our new generic responseArray response serializer. We can add this function
to our GitHubAPIManager:

func getPublicGists() -> Void {


Alamofire.request(GistRouter.GetPublic())
.responseArray {
...
}
}

That might look a bit odd, we just said we’re going to return an array but getPublicGists() has
a return type of Void. That’s because making the request to the API is an asynchronous process:
we fire off the request and get notified when it’s done. We can set up the code with a completion
handler. That lets us add a block of code to be called when the method is done. Our completion
handler needs to handle 2 possibilities: we might return an array of Gists and we might return an
error.
So the signature for the completion handler is (Result<[T], NSError>). That’s a special type that
Alamofire has created to allow us to return either a .Success case with the array of gists or a
.Failure case with an error.

So we’ll fire off our request then apply the responseArray response serializer that we set up above.
That’ll hand us back the gists if it managed to get and parse them or an error if one occurred:

func getPublicGists(completionHandler: (Result<[Gist], NSError>) -> Void) {


Alamofire.request(.GET, "https://api.github.com/gists/public")
.responseArray { (response:Response<[Gist], NSError>) in
...
}
}

Since the responseArray completion handler has a generic type for the array of objects returned
we need to explicitly declare the type when we make this call using result:Response<[Gist],
Hooking Up a REST API to a Table View 65

NSError>. Otherwise when we get to the point of creating objects from the JSON our app would
have no idea what type of object to create.
The completion handler for getPublicGists matches the one for responseArray and there aren’t
any special errors we want to handle right there. So we can just call the completion handler in the
.responseArray block. It’ll be the job of whoever called getPublicGists to handle any errors:

func getPublicGists(completionHandler: (Result<[Gist], NSError>) -> Void) {


Alamofire.request(GistRouter.GetPublic())
.responseArray { (response:Response<[Gist], NSError>) in
completionHandler(response.result)
}
}

Create a function like getPublicGists in your API manager that will return your array of
objects.

Ok, so now we need to figure out when to call getPublicGists. Let’s look at our MasterViewCon-
troller and see how we were firing off our API call earlier:

func loadGists() {
GitHubAPIManager.sharedInstance.printPublicGists()
}

override func viewDidAppear(animated: Bool) {


super.viewDidAppear(animated)
loadGists()
}

It looks like it’ll make sense to replace the call to printPublicGists() with a call to getPublicGists.
That’s easily done:

func loadGists() {
GitHubAPIManager.sharedInstance.getPublicGists() { result in
guard result.error == nil else {
print(result.error)
// TODO: display error
return
}

if let fetchedGists = result.value {


self.gists = fetchedGists
Hooking Up a REST API to a Table View 66

}
self.tableView.reloadData()
}
}

So we fire off the async call to get the gists. If it succeeds then we save the gists in the local array
variable and tell the table view to refresh itself with the new data. Nice & easy.

Set up your function like loadGists to call your function like getPublicGists and save
the resulting array of objects in your MasterViewController so they can be displayed in
the table view.

Ok, so now our API call and our table view code should be fully integrated. Save & run and see what
happens.

Displaying public gists

5.5 And That’s All


Now we have a core app to build on that displays our list of gists. We’ll keep working on it to add:
Hooking Up a REST API to a Table View 67

• Displaying images in table view cells


• Loading more gists as you scroll down
• Pull to refresh
• Detail views for gists
• Deleting gists
• Creating new gists

If you got tired of typing grab the code from GitHub (tagged “tableview”)⁵.
⁵https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/tableview
6. Custom Headers
There are three ways to include custom headers in Alamofire requests: for the session (through the
manager’s configuration), for a single call through the headers argument, or through URLRequest-
Convertible. We’ll show one of each.

6.1 Session Headers


Some headers are commonly send with every request to an API, like an API key or the Accept header.
So it makes sense to set those headers once for the whole session instead of needing to set it for each
request. We can do this through Alamofire’s manager:

let manager = Alamofire.Manager.sharedInstance


manager.session.configuration.HTTPAdditionalHeaders =
["Accept": "application/json"]

Then we just need to make requests using the manager, instead of using the Alamofire class methods,
and the API key header will be passed in each request:

let manager = Alamofire.Manager.sharedInstance


manager.request(...)

instead of

Alamofire.request(...)

So we can reuse the Alamofire manager, we’ll add one to GitHubAPIManager:

68
Custom Headers 69

class GitHubAPIManager {
static let sharedInstance = GitHubAPIManager()
var alamofireManager:Alamofire.Manager

let clientID: String = "1234567890"


let clientSecret: String = "abcdefghijkl"

init () {
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
alamofireManager = Alamofire.Manager(configuration: configuration)
}

...
}

Add an Alamofire manager instance to your API manager class.

6.2 Per Request Headers


What if we want to set a header for a single request? Alamofire now provides an easy way to do
this (as of Alamofire 1.3). First we create a dictionary for our custom headers:

let headers = ["Accept": "application/json"]

Then we pass it in our call to manager.request (or Alamofire.request if we don’t want to include
the session headers we set above):

let urlString = "https://api.github.com/gists"


manager.request(.GET, urlString, headers: headers).responseJSON(...)

or:

let urlString = "https://api.github.com/gists"


Alamofire.request(.GET, urlString, headers: headers).responseJSON(...)

6.3 Headers in URLRequestConvertible


Since we’re using an Alamofire Router to construct our URL requests, we can add headers to the
NSMutableURLRequests. For example:
Custom Headers 70

let URL = NSURL(string: GistRouter.baseURLString)!


let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))

URLRequest.setValue("application/json", forHTTPHeaderField: "Accept")

Then when that URL request gets used the headers are included in the call. We’ll use this approach
later when setting the header for our OAuth token.

6.4 And That’s All For Headers


Headers are used for numerous functions including caching and handling cookies. All three auth
methods that we’ll implement a bit later use headers so we’ll get some practice with them.

Check your documentation for required headers and set them in your API manager
if needed. Don’t worry about authentication headers yet, we’ll handle those in the
authentication chapter.

Before we dig in to authentication we’ll work on improving the basic table view that we’ve already
set up. We’ll add the gist owner’s avatar image to each cell, let users get more gists when they scroll
down, and add pull to refresh so they can easily get the latest gists.
7. Loading UITableViewCell Images
from an API
So far we’ve set up a Swift app that fetched gists from the GitHub API. To do so it:

• Pulls public gists data from the GitHub gists API


• Uses an Alamofire response serializer to process the JSON into an array of Swift objects
• Parses some fields in the web service JSON
• Displays the results in a table view

In this chapter we’ll add a new feature: displaying images of the gist owner’s avatar in each row in
the table view. We’ll get the URLs from the web-based API then load the images asynchronously
from the URLs. We’ll have to handle table view cells getting reused while we’re trying to retrieve
the images and we’ll set up an image cache so we don’t have to pull down the images every time a
cell gets reused.

If your API has images for your main objects then follow along with this section. Otherwise
you might want to skip it and come back when you do need to load images from URLs,
even if they’re not in a table view.

Here’s what it’ll look like when we’re done:

Final cell images

If you haven’t been following along, you might want to grab the code that’ll be our starting point
from GitHub (tagged “headers”)¹.
¹https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/headers

71
Loading UITableViewCell Images from an API 72

Or if you’d rather not type, grab the completed code from this chapter (tagged “cellImages”)².

7.1 Loading UIImages from URLs


We’ve already set up our Gist class to parse out the gist owner’s avatar URL in the init(json:)
function:

self.ownerAvatarURL = json["owner"]["avatar_url"].string

So we’ve got everything set up to retrieve the image URLs. Now we need to get the actual image
data from that URL. Adding to our GitHubAPIManager again:

class GitHubAPIManager
{
...

func imageFromURLString(imageURLString: String, completionHandler:


(UIImage?, NSError?) -> Void) {
alamofireManager.request(.GET, imageURLString)
.response { (request, response, data, error) in
// use the generic response serializer that returns NSData
if data == nil {
completionHandler(nil, nil)
return
}
let image = UIImage(data: data! as NSData)
completionHandler(image, nil)
}
}
}

So we take the imageURLString and use it to make a GET request. When we get the results (as
NSData since we’re using .response) we check that there is data and if so try to turn it in to an
image. If we get an image, we pass that back in the completion handler. If that fails (or there isn’t
an image for our search string), we kick out the error to the completion handler and return.

7.2 UITableViewCell Images from URLs


Now we can hook up displaying images. In MasterViewController’s cellForRowAtIndexPath:

²https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/cellImages
Loading UITableViewCell Images from an API 73

override func tableView(tableView: UITableView,


cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)

let gist = gists[indexPath.row]


cell.textLabel!.text = gist.description
cell.detailTextLabel!.text = gist.ownerLogin
if let urlString = gist.ownerAvatarURL {
GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
(image, error) in
if let error = error {
print(error)
}
if let cellToUpdate = self.tableView?.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image // will work fine even if image is nil
// need to reload the view, which won't happen otherwise
// since this is in an async call
cellToUpdate.setNeedsLayout()
}
})
} else {
cell.imageView?.image = nil
}

return cell
}

We check that we have a URL string for the image: if let urlString = gist.ownerAvatarURL. If
so we fire off the method that we just wrote. If we don’t then we clear out the image for the cell:

GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
...
} else {
cell.imageView?.image = nil
}

In the completion handler we first check for errors, just printing them if we have one. We don’t use
guard here because we should blank out any previous existing image in this case and guard forces
us to return:
Loading UITableViewCell Images from an API 74

completionHandler: { (image, error) in


if let error = error {
print(error)
}
if let cellToUpdate = self.tableView?.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image // will work fine even if image is nil
// need to reload the view, which won't happen otherwise
// since this is in an async call
cellToUpdate.setNeedsLayout()
}
})

And if we don’t have an error we set the image that we received as the cell’s image. This is a bit
complicated by the fact that our imageFromURLString is asynchronous and by how UITableView
reuses cells. Since we’re using dequeueReusableCellWithIdentifier the table view will reuse cells
that have scrolled off of the screen. For example, if we have 20 gists but can only see 10 cells on
the screen at once the table view will only create about 12-14 cells. If our cell has scrolled off of the
screen then we shouldn’t set the image, since it’ll be for the wrong gist.
We can work around this problem by using the index path once we’ve got the image, asking the
table view for the cell for the index path (row and section):

if let cellToUpdate = self.tableview?.cellForRowAtIndexPath(indexPath)`

Then we can set the image on that cell. If the cell is currently being shown, that’ll give us the cell
that’s on the screen for our index path. Since we’re all async here we also need to tell the cell that
we’ve changed part of its view and it needs to redraw itself using cellToUpdate.setNeedsLayout().
Note that we don’t need to check if our image is nil before setting it to the cell’s imageView. If
the image is nil for some reason then we’ll blank out the cell’s image when we do cellToUp-
date.imageView?.image = result.value, which is really the best we can do in that case.

Save and run. You should now see avatars for each gist, if the user has set one:
Loading UITableViewCell Images from an API 75

Avatars

If your API has images, set them up to load as we did in this section.

7.3 Enhancements
We’re firing off requests to get the images for each cell but sometimes by the time we get the result
we don’t need it anymore. An optimization would be to be cancel the Alamofire requests if the cell
scrolls off of the screen. If you’re dealing with lots of images you’d want to do that (you’ll know
you need it if the scrolling isn’t smooth). Our scrolling seems pretty smooth and we’re going to add
a cache, so this concern isn’t one that needs to be addressed right now.
An optimization that is worthwhile even for this small app is caching the images so we don’t have to
grab them from the web every time they’re shown. We’ll do a quick and easy single-run-of-the-app
cache to see how it will work. Then we’ll replace our cache with PINRemoteImage which will give
us a smarter persistent cache.
Loading UITableViewCell Images from an API 76

7.4 Caching Images


In our MasterViewController we can add a dictionary to hold the images indexed by their URL:

var imageCache: Dictionary<String, UIImage?> = Dictionary<String, UIImage?>()

Now we can save the images when we get them (in our cellForRowAtIndexPath function):

if let urlString = gist.ownerAvatarURL {


GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
(image, error) in
if let error = error {
print(error)
}
// Save the image so we won't have to keep fetching it if they scroll
self.imageCache[urlString] = image
if let cellToUpdate = self.tableView?.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image // will work fine even if image is nil
// need to reload the view, which won't happen otherwise
// since this is in an async call
cellToUpdate.setNeedsLayout()
}
})
} else {
cell.imageView?.image = nil
}

And then before retrieving the image, we’ll check the cache to see if we already have it:

if let urlString = gist.ownerAvatarURL {


if let cachedImage = imageCache[urlString] {
cell.imageView?.image = cachedImage // will work fine even if image is nil
} else {
GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
...
})
}
} else {
cell.imageView?.image = nil
}

So finally:
Loading UITableViewCell Images from an API 77

override func tableView(tableView: UITableView, cellForRowAtIndexPath


indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)

let gist = gists[indexPath.row]


cell.textLabel!.text = gist.description
cell.detailTextLabel!.text = gist.ownerLogin
// set cell.imageView to display image at gist.ownerAvatarURL
if let urlString = gist.ownerAvatarURL {
if let cachedImage = imageCache[urlString] {
cell.imageView?.image = cachedImage // will work fine even if image is nil
} else {
GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
(image, error) in
if let error = error {
print(error)
}
// Save the image so we won't have to keep fetching it if they scroll
self.imageCache[urlString] = image
if let cellToUpdate = self.tableView?.cellForRowAtIndexPath(indexPath) {
cellToUpdate.imageView?.image = image // will work fine even if image is nil
// need to reload the view, which won't happen otherwise
// since this is in an async call
cellToUpdate.setNeedsLayout()
}
})
}
} else {
cell.imageView?.image = nil
}

return cell
}

To test out this code you’ll have to set a breakpoint and step through to see which lines get executed.
To add a breakpoint, click on the line number next to the code where you want the code execution
to stop:
Loading UITableViewCell Images from an API 78

Adding a breakpoint

Then run the app. When it gets to the breakpoint the code will stop running. At that point you can
use the panel at the bottom of Xcode to examine variables and step through the code:

Stopped at breakpoint

Click on the “step over” button to go to the next line of code:

Step over button

Watch which code path gets taken by seeing which line is highlighted. If there is an avatarURL it’ll
try to load it:
Loading UITableViewCell Images from an API 79

Load image

If there isn’t one then it’ll set the cell image to nil:

No image

To resume the program until the next time the breakpoint is hit you can click the continue button:

Continue button

When you run the app all of the images will get loaded from their URLs the first time that they’re
displayed because we’re not persisting the image cache between runs of the app. We’ll fix that in the
next session. Since table view cells get reused our cache will kick in when we scroll cells on and off
of the screen. The easiest way to see when the cache gets used is to move the breakpoint to where
we’re loading the image from the cache:

Loading image from cache


Loading UITableViewCell Images from an API 80

After the initial cells load scroll up and down. You should hit the breakpoint and see that line getting
executed.

7.5 A Better Cache: PINRemoteImage


We’ve set up our Alamofire calls to parse an image URL out of JSON as a string, then to load the
image from the URL. We then used those images in UITableViewCells, handling the asynchronous
loading even though the cells might have been reused. We set up a primitive cache for a single run
of the app that helps us avoid constantly reloading the images. Before using this code in a released
app it would be worth setting up a persistent cache that’s used for multiple runs of the app.
PINRemoteImage³ is a great new persistent image caching library so let’s use that. Using this cache
will make sure that images are only loaded once and it’s simpler to integrate than our non persistent
cache.
Add PINRemoteImage v1.2 to your project using CocoaPods. Import it into the MasterViewCon-
troller file and delete our simple cache:

import UIKit
import PINRemoteImage

class MasterViewController: UITableViewController {


...

var imageCache: Dictionary<String, UIImage?> = Dictionary<String, UIImage?>()

...
}

PINRemoteImage works best with a placeholder image and it’s a nice touch too. So toss a small
square image into your app (just drag & drop it in then select the option to copy it into your project).
Rename the image to “placeholder.png”. Then we can remove the imageCache above and simplify
the cell code. We’ll also display the placeholder for users who don’t have an avatar set:

³https://github.com/pinterest/PINRemoteImage
Loading UITableViewCell Images from an API 81

override func tableView(tableView: UITableView, cellForRowAtIndexPath


indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)

let gist = gists[indexPath.row]


cell.textLabel!.text = gist.description
cell.detailTextLabel!.text = gist.ownerLogin
cell.imageView?.image = nil

// set cell.imageView to display image at gist.ownerAvatarURL


if let urlString = gist.ownerAvatarURL, url = NSURL(string: urlString) {
cell.imageView?.pin_setImageFromURL(url, placeholderImage:
UIImage(named: "placeholder.png"))
} else {
cell.imageView?.image = UIImage(named: "placeholder.png")
}

return cell
}

7.6 And That’s All


Save and run to test that out. You should end up with some avatars loaded and your placeholder for
those who don’t have avatars:

Cell images

If you got tired of typing, here’s the code with PINRemoteImage: (tagged “cellImages”)⁴.
⁴https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/cellImages
8. Pagination, a.k.a., Load More on
Scroll
When GitHub gives us data it only sends a limited amount at a time. For example, if we ask for all
of the public gists that exist GitHub won’t actually give us all of them. It’ll give us the 16 or so most
recent gists. If we want to show more than those gists then we need to hit the API again to get more.

8.1 Where is the Next Page?


First we need to figure out how GitHub provides more of the results. Fortunately they’ve got some
solid documentation on their Pagination¹. The short & simple explanation is that you add ?page=2
to the API endpoint, then ?page=3, then ?page=4, …
The more correct answer is that you should check the headers in the response, specifically the link
header. For our request for public gists the link header looks like this:

<https://api.github.com/gists/public?page=2>; rel="next",
<https://api.github.com/gists/public?page=100>; rel="last"

So to get the next set of results we call https://api.github.com/gists/public?page=2. If we make


that call then the link header gets more complicated:

<https://api.github.com/gists/public?page=3>; rel="next",
<https://api.github.com/gists/public?page=100>; rel="last",
<https://api.github.com/gists/public?page=1>; rel="first",
<https://api.github.com/gists/public?page=1>; rel="prev"

For our load more function we only need the next url, so let’s extract that:

¹https://developer.github.com/guides/traversing-with-pagination/

82
Pagination, a.k.a., Load More on Scroll 83

private func getNextPageFromHeaders(response: NSHTTPURLResponse?) -> String? {


if let linkHeader = response?.allHeaderFields["Link"] as? String {
/* looks like:
<https://api.github.com/user/20267/gists?page=2>; rel="next", <https://api.github.com/\
user/20267/gists?page=6>; rel="last"
*/
// so split on "," then on ";"
let components = linkHeader.characters.split {$0 == ","}.map { String($0) }
// now we have 2 lines like
// '<https://api.github.com/user/20267/gists?page=2>; rel="next"'
// So let's get the URL out of there:
for item in components {
// see if it's "next"
let rangeOfNext = item.rangeOfString("rel=\"next\"", options: [])
if rangeOfNext != nil {
let rangeOfPaddedURL = item.rangeOfString("<(.*)>;",
options: .RegularExpressionSearch)
if let range = rangeOfPaddedURL {
let nextURL = item.substringWithRange(range)
// strip off the < and >;
let startIndex = nextURL.startIndex.advancedBy(1)
let endIndex = nextURL.endIndex.advancedBy(-2)
let urlRange = startIndex..<endIndex
return nextURL.substringWithRange(urlRange)
}
}
}
}
return nil
}

Ok, that looks a bit complicated. Let’s start at the top and walk through it. First we’re declaring a
function that’ll take the NSHTTPURLResponse and extract the next page header as a String:

private func getNextPageFromHeaders(response: NSHTTPURLResponse?) -> String? {

First we get the headers from the response to our request:

if let linkHeader = response?.allHeaderFields["Link"] as? String {


...
}

That header is made up of components like <URL>; rel="type", separated by commas. So first we’ll
split them by the commas to get those components into an array that we can loop through:
Pagination, a.k.a., Load More on Scroll 84

// so split on "," then on ";"


let components = linkHeader.characters.split {$0 == ","}.map { String($0) }
for item in components {
...
}

Then as we loop through the components, we’ll find the one with the next url by checking if it has
rel="next" in it:

for item in components {


// see if it's "next"
let rangeOfNext = item.rangeOfString("rel=\"next\"", options: [])
if rangeOfNext != nil {
// found the component with the next URL
...
}
}

Then we need to parse that component to draw out the next URL. We can do that using regular
expressions to match the pattern that we expect. Regular expressions² are special string patterns
that describe how to search through a string. For example, our URL is wrapped in a few characters
like <(.*)>;, where (.*) is the URL. <(.*)>; is the regular expression that describes how to find
the URL in the text. So we can search for that pattern and we’ll find our URL. Then we’ll have to
remove those few characters that aren’t part of the URL < and >;:

let rangeOfNext = item.rangeOfString("rel=\"next\"", options: [])


if rangeOfNext != nil {
let rangeOfPaddedURL = item.rangeOfString("<(.*)>;",
options: .RegularExpressionSearch)
if let range = rangeOfPaddedURL {
let nextURL = item.substringWithRange(range)
// strip off the < and >;
let startIndex = nextURL.startIndex.advancedBy(1)
let endIndex = nextURL.endIndex.advancedBy(-2)
let urlRange = startIndex..<endIndex
return nextURL.substringWithRange(urlRange)
}
}

²https://en.wikipedia.org/wiki/Regular_expression
Pagination, a.k.a., Load More on Scroll 85

8.2 Fetching and Appending


Now we know the URL to call to get more results when the user scrolls down. But when should we
be calling it? We’ll need to make the URL available to whoever is calling this function, so we need
to add it to the completion handler. That will extend the completion handler to also return a string
(the next url):

func getPublicGists(completionHandler: (Result<[Gist], NSError>, String?) -> Void) {


alamofireManager.request(GistRouter.GetPublic())
.validate()
.responseArray { (response:Response<[Gist], NSError>) in
guard response.result.error == nil,
let gists = response.result.value else {
print(response.result.error)
completionHandler(response.result, nil)
return
}

// need to figure out if this is the last page


// check the link header, if present
let next = self.getNextPageFromHeaders(response.response)
completionHandler(.Success(gists), next)
}
}

Now we need to be able to load the later pages of gists. That requires passing in a URL and loading
gists from it. We can generalize the code in getGists and getPublicGists to do that:

func getGists(urlRequest: URLRequestConvertible, completionHandler:


(Result<[Gist], NSError>, String?) -> Void) {
alamofireManager.request(urlRequest)
.validate()
.responseArray { (response:Response<[Gist], NSError>) in
guard response.result.error == nil,
let gists = response.result.value else {
print(response.result.error)
completionHandler(response.result, nil)
return
}

// need to figure out if this is the last page


// check the link header, if present
let next = self.getNextPageFromHeaders(response.response)
Pagination, a.k.a., Load More on Scroll 86

completionHandler(.Success(gists), next)
}
}

func getPublicGists(pageToLoad: String?, completionHandler:


(Result<[Gist], NSError>, String?) -> Void) {
if let urlString = pageToLoad {
getGists(GistRouter.GetAtPath(urlString), completionHandler: completionHandler)
} else {
getGists(GistRouter.GetPublic(), completionHandler: completionHandler)
}
}

We’ll also need to extend the Router to generate the URL requests for GetAtPath:

enum GistRouter: URLRequestConvertible {


static let baseURLString:String = "https://api.github.com"

case GetPublic() // GET https://api.github.com/gists/public


case GetAtPath(String) // GET at given path

var URLRequest: NSMutableURLRequest {


var method: Alamofire.Method {
switch self {
case .GetPublic:
return .GET
case .GetAtPath:
return .GET
}
}

let result: (path: String, parameters: [String: AnyObject]?) = {


switch self {
case .GetPublic:
return ("/gists/public", nil)
case .GetAtPath(let path):
let URL = NSURL(string: path)
let relativePath = URL!.relativePath!
return (relativePath, nil)
}
}()

let URL = NSURL(string: GistRouter.baseURLString)!


let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))
Pagination, a.k.a., Load More on Scroll 87

let encoding = Alamofire.ParameterEncoding.JSON


let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)

encodedRequest.HTTPMethod = method.rawValue

return encodedRequest
}
}

Getting the path for GetAtPath is a little tricky since we already have the full URL. Fortunately
NSURL lets us grab the relative path. Alternatively we could have modified the let URL = ... code
to use the full path that was passed.

If you need to, figure out how you can get more results from your API. Modify your existing
functions like getGists and getPublicGists in your API manager to handle it. Instead of
a next URL you might need to pass in the page number explicitly, the number of objects
you’ve already loaded, or the ID of the last object that you’ve loaded. Pagination is one
of those features that can vary a lot between APIs but you should still be able to use the
framework presented here.

8.3 Integrating with the View Controller


That should do it for the GitHubAPIManager. Now switch over to our MasterViewController. We’ll
have to update all of those calls to get gists and save the next URL if we get one. We’ll also need to
able to pass in a URL to load from:

class MasterViewController: UITableViewController {


...
var nextPageURLString: String?

...
}

Then we pass through the url to load when we call loadGists, even if it’s nil:
Pagination, a.k.a., Load More on Scroll 88

func loadGists(urlToLoad: String?) {


GitHubAPIManager.sharedInstance.getPublicGists(urlToLoad) { (result, nextPage) in
self.nextPageURLString = nextPage

guard result.error == nil else {


print(result.error)
// TODO: display error
return
}

if let fetchedGists = result.value {


self.gists = fetchedGists
}
self.tableView.reloadData()
}
}

See any problems with that? How about this bit?

if let fetchedGists = result.value {


self.gists = theGists
}

What if we’re trying to get the second page of gists? This code would replace the existing ones
instead of appending the new ones. Let’s fix that:

if let fetchedGists = result.value {


if self.nextPageURLString != nil {
self.gists += fetchedGists
} else {
self.gists = fetchedGists
}
}

That’s better.
We also need to pass in nil in viewDidAppear so the first page of gists gets loaded:
Pagination, a.k.a., Load More on Scroll 89

override func viewDidAppear(animated: Bool) {


super.viewDidAppear(animated)

loadGists(nil)
}

8.4 When to Load More Gists?


So that’s all nicely set up but when do we actually call the function to get more gists? Let’s set it up
so we’ll load more gists if they’re scrolled down far enough that there are only 5 more rows to the
bottom:

override func tableView(tableView: UITableView, cellForRowAtIndexPath


indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)

let gist = gists[indexPath.row]


cell.textLabel!.text = gist.description
cell.detailTextLabel!.text = gist.ownerLogin
cell.imageView?.image = nil

// set cell.imageView to display image at gist.ownerAvatarURL


if let urlString = gist.ownerAvatarURL, url = NSURL(string: urlString) {
cell.imageView?.pin_setImageFromURL(url, placeholderImage:
UIImage(named: "placeholder.png"))
} else {
cell.imageView?.image = UIImage(named: "placeholder.png")
}

// See if we need to load more gists


let rowsToLoadFromBottom = 5;
let rowsLoaded = gists.count
if let nextPage = nextPageURLString {
if (!isLoading && (indexPath.row >= (rowsLoaded - rowsToLoadFromBottom))) {
self.loadGists(nextPage)
}
}

return cell
}

Here’s the meat of that:


Pagination, a.k.a., Load More on Scroll 90

let rowsToLoadFromBottom = 5;
let rowsLoaded = gists.count
if let nextPage = nextPageURLString {
if (!isLoading && (indexPath.row >= (rowsLoaded - rowsToLoadFromBottom))) {
self.loadGists(nextPage)
}
}

If they’re within 5 rows of the bottom and we have a URL for the next page of results, then we should
load more results (unless we’re already loading gists, then just hold off). To load more results we
call loadGists(nextPage) with the URL that we want to fetch from.
The isLoading variable needs to be added so that we don’t fire off loading more rows while we’re
already loading more rows:

class MasterViewController: UITableViewController {

var detailViewController: DetailViewController? = nil


var gists = [Gist]()
var nextPageURLString: String?
var isLoading = false
...
}

And set it true while we load gists:

func loadGists(urlToLoad: String?) {


self.isLoading = true
GitHubAPIManager.sharedInstance.getPublicGists(urlToLoad) { (result, nextPage) in
self.isLoading = false
self.nextPageURLString = nextPage

guard result.error == nil else {


print(result.error)
// TODO: display error
return
}

if let fetchedGists = result.value {


if urlToLoad != nil {
self.gists += fetchedGists
} else {
self.gists = fetchedGists
}
Pagination, a.k.a., Load More on Scroll 91

}
self.tableView.reloadData()
}
}

Modify your table view to load more objects as the user scrolls down close to the bottom of
the list. You’ll have to keep track of whatever parameter your API uses for pagination, e.g.,
a next URL, a page number, the number of objects loaded, the ID of the last object loaded,

8.5 And That’s All for Pagination


While it was a bit of work to hook in to our API calls, that’s all it takes to set up loading more on
scroll for an API-backed table view. Save and run. You should be able to scroll down and see more
gists than just the ones that originally load:

Lots of Gists

If you got tired of typing, here’s the code: (tagged “pagination”)³.


³https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/pagination
9. Pull to Refresh
Adding pull to refresh to a UITableView sounds like a lot more work than it actually is. The iOS
UIRefreshControl makes it a quick and easy feature to implement. In this chapter we’ll add pull to
refresh to get updates to our lists of gists. When we’re done it’ll look like this:

Pull to refresh with date

9.1 Adding Pull to Refresh


In iOS UITableView and UIRefreshControl are designed for each other. In fact, UITableViewCon-
troller already has a refreshControl property, it’s just not initialized by default. So we can just
create a refresh control and assign it to that property in our MasterViewController. We’ll also set
it to call a function called refresh when the user activates it:

92
Pull to Refresh 93

override func viewWillAppear(animated: Bool) {


self.clearsSelectionOnViewWillAppear = self.splitViewController!.collapsed

// add refresh control for pull to refresh


if (self.refreshControl == nil) {
self.refreshControl = UIRefreshControl()
self.refreshControl?.addTarget(self, action: "refresh:",
forControlEvents: UIControlEvents.ValueChanged)
}

super.viewWillAppear(animated)
}

And add our refresh function:

// MARK: - Pull to Refresh


func refresh(sender:AnyObject) {
nextPageURLString = nil // so it doesn't try to append the results
loadGists(nil)
}

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

func loadGists(urlToLoad: String?) {


self.isLoading = true
GitHubAPIManager.sharedInstance.getPublicGists(urlToLoad) { (result, nextPage) in
self.isLoading = false
self.nextPageURLString = nextPage

// tell refresh control it can stop showing up now


if self.refreshControl != nil && self.refreshControl!.refreshing {
self.refreshControl?.endRefreshing()
}

guard result.error == nil else {


print(result.error)
// TODO: display error
return
}

if let fetchedGists = result.value {


if urlToLoad != nil {
self.gists += fetchedGists
Pull to Refresh 94

} else {
self.gists = fetchedGists
}
}
self.tableView.reloadData()
}
}

Save and run to test. If you’re happy with the refresh control as it is then you’re done:

Pull to refresh with image

9.2 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:
Pull to Refresh 95

class MasterViewController: UITableViewController {


var dateFormatter = NSDateFormatter()
// ...

override func viewWillAppear(animated: Bool) {


self.clearsSelectionOnViewWillAppear = self.splitViewController!.collapsed

super.viewWillAppear(animated)

// add refresh control for pull to refresh


if (self.refreshControl == nil) {
self.refreshControl = UIRefreshControl()
self.refreshControl?.attributedTitle = NSAttributedString(string: "Pull to refresh")
self.refreshControl?.addTarget(self, action: "refresh:",
forControlEvents: UIControlEvents.ValueChanged)
self.dateFormatter.dateStyle = NSDateFormatterStyle.ShortStyle
self.dateFormatter.timeStyle = NSDateFormatterStyle.LongStyle
}
}
}

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

func loadGists(urlToLoad: String?) {


self.isLoading = true
self.nextPageURLString = nextPage
GitHubAPIManager.sharedInstance.getPublicGists(urlToLoad) { (result, nextPage) in
self.isLoading = false
// tell refresh control it can stop showing up now
if self.refreshControl != nil && self.refreshControl!.refreshing {
self.refreshControl?.endRefreshing()
}

guard result.error == nil else {


print(result.error)
self.nextPageURLString = nil
return
}

if let fetchedGists = result.value {


if urlToLoad != nil {
self.gists += fetchedGists
} else {
self.gists = fetchedGists
}
Pull to Refresh 96

// 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)

self.tableView.reloadData()
}
}

9.3 And That’s All


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

Pull to refresh with date

If you got tired of typing, here’s the code: (tagged “pull_to_refresh”)¹.


¹https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/pull_to_refresh
10. Authentication
If you’re building an app based on a REST API you’re probably going to need to authenticate at
some point. You might even need to authenticate to make any REST calls at all. Using the GitHub
API, we’ll figure out how to set up two types of authentication: basic auth and OAuth 2.0. Our gists
app will eventually use the OAuth 2.0 authentication. We’ll also cover how to use header/token auth
using Mashape¹.

10.1 The Docs


If you likes to dig into documentation, read up on the GitHub API docs for authentication².
If we check out the docs we’ll find a few key points:

• The GitHub API can be used with Basic Auth or OAuth 2.0
• If we try to authenticate with invalid credentials we’ll get a 401 Unauthorized response
• Requests that require authentication will return 404 Not Found, instead of 403 Forbidden, in
some places.

Check your documentation for authentication requirements. Implement the section of this
chapter that matches your API. Also implement the integrating the authentication in your
API manager as shown near the end of this chapter.

10.2 Basic Auth: Username/Password


We’ll eventually be adding OAuth 2.0 but we’ll cover how to use basic auth first. For now we’ll just
print the API results to confirm that we’ve succeeded, similar to when we set up printPublicGists.
We’ll set up returning the arrays of gists for the table view to display when we implement OAuth
2.0 later in this chapter.
Let’s add a declaration for a function that will make an API call using basic auth:

¹https://www.mashape.com/
²https://developer.github.com/v3/#authentication

97
Authentication 98

class GitHubAPIManager {

// MARK: - Basic Auth


func printMyStarredGistsWithBasicAuth() -> Void {
// TODO: implement
}
}

We’ll need to add the call to get starred gists to our router:

enum GistRouter: URLRequestConvertible {


static let baseURLString:String = "https://api.github.com"

case GetPublic() // GET https://api.github.com/gists/public


case GetMyStarred() // GET https://api.github.com/gists/starred

case GetAtPath(String) // GET at given path

var URLRequest: NSMutableURLRequest {


var method: Alamofire.Method {
switch self {
case .GetPublic:
return .GET
case .GetMyStarred:
return .GET
case .GetAtPath:
return .GET
}
}

let result: (path: String, parameters: [String: AnyObject]?) = {


switch self {
case .GetPublic:
return ("/gists/public", nil)
case .GetMyStarred:
return ("/gists/starred", nil)
case .GetAtPath(let path):
let URL = NSURL(string: path)
let relativePath = URL!.relativePath!
return (relativePath, nil)
}
}()

let URL = NSURL(string: GistRouter.baseURLString)!


let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))
Authentication 99

let encoding = Alamofire.ParameterEncoding.JSON


let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)

encodedRequest.HTTPMethod = method.rawValue

return encodedRequest
}
}

Let’s fill that in like we did for getting public gists (without any authentication) and see what
happens. In GitHubAPIController:

func printMyStarredGistsWithBasicAuth() -> Void {


Alamofire.request(GistRouter.GetMyStarred())
.responseString { response in
if let receivedString = response.result.value {
print(receivedString)
}
}
}

And in the MasterViewController change viewDidAppear to test this function:

override func viewDidAppear(animated: Bool) {


super.viewDidAppear(animated)
loadGists(nil)

// TEST
GitHubAPIManager.sharedInstance.printMyStarredGistsWithBasicAuth()
// END TEST
}

If we save & run that we get an error in the console:

{
"message":"Requires authentication",
"documentation_url":"https://developer.github.com/v3/#authentication"
}

So, as the docs said, we need to add authentication. The easiest way is to supply our username and
password, i.e., using Basic Authentication³. Basic auth requires setting an Authorization header
that contains our username:password with base64 encoding.
³https://en.wikipedia.org/wiki/Basic_access_authentication
Authentication 100

Base64 encoding is a fairly simple way of encoding binary data to send it as text. This type of
encoding doesn’t provide additional security since it’s an encoding, not an encryption. So why is
it used in basic auth when the username and password are already text? Mostly to encode any odd
characters that might cause issues with the HTTP request.
.

Let’s start by Base64 encoding our credentials, then we’ll add the Authorization HTTP header to
our request. Make sure to use your GitHub username and password. We’ll set up the authentication
in the router where we’re creating the NSMutableURLRequest:

let URL = NSURL(string: GistRouter.baseURLString)!


let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))

let username = "myUsername"


let password = "myPassword"

let credentialData = "\(username):\(password)".dataUsingEncoding(NSUTF8StringEncoding)!


let base64Credentials = credentialData.base64EncodedStringWithOptions([])

...

So first we set up the string we want to encode: "\(username):\(password)". Then we turn it into
NSData using the common NSUTF8StringEncoding string encoding. That just converts our string to
binary data so we can apply the Base64 encoding. To actually perform the Base64 encoding, we call
the base64EncodedStringWithOptions function on our data.
So how do we send the header? Check back in the headers chapter for details.

...

URLRequest.setValue("Basic \(base64Credentials)", forHTTPHeaderField: "Authorization")

let encoding = Alamofire.ParameterEncoding.JSON


let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)

encodedRequest.HTTPMethod = method.rawValue

return encodedRequest

Build & run and you should get back a list of the Gists you’ve starred (probably a good idea
to go star some Gists⁴ before testing that code). You can see a list of your starred gists at
https://gist.github.com/*username*/starred:
⁴https://gist.github.com/search?utf8=%E2%9C%93&q=swift&ref=searchresults
Authentication 101

[{"url":"https://api.github.com/gists/3bbe842793ac3f692775","forks_url":"https://api.githu\
b.com/gists/3bbe842793ac3f692775/forks","commits_url":"https://api.github.com/gists/3bbe84\
2793ac3f692775/commits","id":"3bbe842793ac3f692775","git_pull_url":"https://gist.github.co\
m/3bbe842793ac3f692775.git",...

Depending on your API, sometimes you can use the Alamofire .authenticate function to make
sending basic auth credentials even simpler. Sadly this doesn’t work with GitHub’s API but we can
use the HTTPBin test API⁵ to try it out. To do so, chain an extra function into your Alamofire
request: .authenticate(user: username, password: password). That function call will pack up
the inputs into the appropriate header for us and handle auth challenges:

func doGetWithBasicAuth() -> Void {


let username = "myUsername"
let password = "myPassword"
Alamofire.request(.GET, "https://httpbin.org/basic-auth/\(username)/\(password)")
.authenticate(user: username, password: password)
.responseString { response in
if let receivedString = response.result.value {
print(receivedString)
}
}
}

Normally you wouldn’t need to put your username and password into the URL in the request, it’s
only done here to use the HTTPBin⁶ testing function.
Alternatively, you can pack up the username and password in an NSURLCredential:

func doGetWithBasicAuthCredential() -> Void {


let username = "myUsername"
let password = "myPassword"

let credential = NSURLCredential(user: username, password: password,


persistence: NSURLCredentialPersistence.ForSession)

Alamofire.request(.GET, "https://httpbin.org/basic-auth/\(username)/\(password)")
.authenticate(usingCredential: credential)
.responseString { response in
if let receivedString = response.result.value {
print(receivedString)
}
}
}

⁵https://httpbin.org/
⁶https://httpbin.org/
Authentication 102

Using a credential lets you specify how long the authentication should live (one call, one session
or forever) as NSURLCredentialPersistence. It also works with certificate-based authentication, if
you’re headed down that road.

If your API uses basic authentication, set it up using a credential that you can store. The
section on OAuth later in this chapter shows how to use Locksmith⁷ to save sensitive
information in the iOS Keychain. You’ll also need to add a view that pops up to collect
the username and password. Check out the gist creation form to see how to add a simple
form. You’ll want it to pop up when you launch the app or make an API call and don’t
have a credential saved. We’ll do something similar in the OAuth 2.0 code so check how
we’re starting the login process there.

10.3 HTTP Header Authentication


Some APIs use HTTP headers for authentication. For example, Mashape⁸ is a great marketplace to
find APIs to use to test your skills. For example, you can access the Urban Dictionary API through
Mashape⁹.
When you sign up for a Mashape account you’ll get 2 headers you need to pass with your API calls
instead of using Basic Authentication:

• X-Mashape-Key: MY_API_KEY
• Accept: application/json

This curl statement works:

curl --get --include \


'https://mashape-community-urban-dictionary.p.mashape.com/define?term=hipster' \
-H 'X-Mashape-Key: MY_API_KEY' \
-H 'Accept: application/json'

So how can we make this call work with Alamofire? Remember the headers chapter? It has all the
details that we need. Those headers should be included with the whole session so we’ll do that.
Often, if an API key is needed it then it needs to be provided with all of the calls to an API. So it
makes sense to set it once for the whole session instead of needing to set it for each request.
So far we’ve been mostly making requests using Alamofire.request(...). Behind the scenes
Alamofire uses a single manager to handle these calls. It would do exactly the same thing if we
call that manager explicitly like this:
⁷https://github.com/matthewpalmer/Locksmith
⁸https://www.mashape.com/
⁹https://www.mashape.com/community/urban-dictionary
Authentication 103

let manager = Alamofire.Manager.sharedInstance


manager.request(...)

And that lets us work with the manager directly so we can do things like setting HTTP headers for
the whole session, like we did with the GitHub accept header.
Now if we set up a manager to work with Mashape:

class MashapeAPIManager {
static let sharedInstance = MashapeAPIManager()

var alamofireManager:Alamofire.Manager

init () {
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()

configuration.HTTPAdditionalHeaders = Manager.defaultHTTPHeaders

alamofireManager = Alamofire.Manager(configuration: configuration)


}
}

We can add the headers that we need:

init () {
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()

var headers = Manager.defaultHTTPHeaders


headers["X-Mashape-Key"] = "MY_API_KEY"
headers["Accept"] = "application/json"
configuration.HTTPAdditionalHeaders = headers

alamofireManager = Alamofire.Manager(configuration: configuration)


}

Then when we use:

let manager = MashapeAPIManager.sharedInstance.alamofireManager


manager.request(.GET,
"https://mashape-community-urban-dictionary.p.mashape.com/define?term=hipster")

Instead of:
Authentication 104

Alamofire.request(.GET,
"https://mashape-community-urban-dictionary.p.mashape.com/define?term=hipster")

The "X-Mashape-Key": "MY_API_KEY" header and the JSON accept header will get passed with every
request.

If your API uses token/header based auth then set up your session headers in your API
manager class.

If you’re looking for APIs to play with to develop your skills, check our Mashape. They have tons
of free APIs for your coding pleasure.
https://www.mashape.com/explore?page=1&#038;price=free

10.4 Alamofire Validation


Alamofire has a .validate() method that can be chained into your requests like so:

alamofireManager.request(GistRouter.GetMyStarred())
.validate()
.responseString { response in
guard response.result.error == nil else {
print(response.result.error!)
}
if let receivedString = response.result.value {
print(receivedString)
}
}

If you don’t include the call to .validate then Alamofire will assume that the call succeeded. The
.validate() call only fails if the content-type of the response isn’t what we expect or the status
code isn’t 200-299. In older versions of Alamofire validate could hide more descriptive errors since
your custom serializers wouldn’t get called if there was already an error. As of v3.0.0 that’s no longer
the case.
Let’s see how we can get the most useful error messages from the GitHub gists API.
print(response.result.error!) prints:
Authentication 105

Error Domain=com.alamofire.error Code=-1


"The operation couldn't be completed. (com.alamofire.error error -1.)"

Then print(receivedString) prints the more informative error message:

{
"message":"Bad credentials",
"documentation_url":"https://developer.github.com/v3"
}

With this API we’re best off checking for errors in the JSON before returning the error from
.validate(). Let’s do that:

alamofireManager.request(GistRouter.GetMyStarred())
.validate()
.responseString { response in
if let receivedString = response.result.value {
print(receivedString)
let json = SwiftyJSON.JSON(receivedString)
// Check for error in JSON
if let message = json["message"].string {
let error = Error.errorWithCode(.DataSerializationFailed, failureReason: message)
// TODO: bubble up error
}
// Do other stuff with JSON
}
if error = response.result.error else {
print(error)
// TODO: bubble up error
}
}

Now using .validate() won’t hide any errors that come in the JSON response.

With some APIs you might be more informative errors in your JSON than from
.validate(). Test your API by printing out the errors from your JSON and from
.validate() to see whether you should use a guard statement to return from errors
immediately in your response serializer or whether you should parse the JSON and return
the message from it. You might find issues like missing headers returning “The operation
couldn’t be completed” instead of “unauthorized” (e.g., the Parse REST API¹⁰). Certainly
if we can get a more descriptive error we should display that instead of the generic
networking error.

¹⁰http://parse.com
Authentication 106

10.5 OAuth 2.0


OAuth 2.0 is super common for authentication these days, especially since it lets you login without
giving your password to every app you use or creating new accounts for every app. If you’re not
familiar with OAuth check out the great explanation on RayWenderlich.com¹¹. Here’s a simple
outline of how it works:
If you wanted to let an iOS app have some access to your Twitter account, then the OAuth 2.0
authentication flow would be:

1. The app sends you to Twitter to login


2. You login to Twitter to authorize the app (possibly giving it specific limited permissions)
3. Twitter sends you back to the app with a token for that app to use

The flow can be a little confusing (in fact, there’s an extra step that we’ll add later) but it means
that the iOS app never knows your password. It also lets you revoke its permission later without
changing your Twitter password.
When building any app around an API with OAuth 2.0 authentication the first thing you need to do
is setting up that login flow to get a token. So that’s what we’ll do now.
We’re working with the GitHub gists API call to get a list of our starred gists. The endpoint for
that call is https://api.github.com/gists/starred¹². Without authentication we get this error as the
response:

{
"message":"Bad credentials",
"documentation_url":"https://developer.github.com/v3"
}

That error tells us that we need to get an OAuth token to send along with that request. So let’s do
that. We’ll set up the request to print our starred gists, just like we did for basic auth. Then we’ll
implement the OAuth login flow and the authenticated call to get a list of gists.
Fair warning: This section is kinda long. You might want to go to the washroom first or grab a snack.
Here’s the API call without authentication:

¹¹http://www.raywenderlich.com/99431/OAuth-2-with-swift-tutorial
¹²https://api.github.com/gists/starred
Authentication 107

// MARK: - OAuth 2.0


func printMyStarredGistsWithOAuth2() -> Void {
alamofireManager.request(GistRouter.GetMyStarred())
.responseString { response in
guard response.result.error == nil else {
print(response.result.error!)
return
}
if let receivedString = response.result.value {
print(receivedString)
}
}
}

If you added the basic auth bits to your Router earlier in this chapter, remove them now. We’ll be
replacing them with an OAuth token shortly. Here’s what our router looks like before adding any
authentication:

enum GistRouter: URLRequestConvertible {


static let baseURLString:String = "https://api.github.com"

case GetPublic() // GET https://api.github.com/gists/public


case GetMyStarred() // GET https://api.github.com/gists/starred
case GetAtPath(String) // GET at given path

var URLRequest: NSMutableURLRequest {


var method: Alamofire.Method {
switch self {
case .GetPublic:
return .GET
case .GetMyStarred:
return .GET
case .GetAtPath:
return .GET
}
}

let result: (path: String, parameters: [String: AnyObject]?) = {


switch self {
case .GetPublic:
return ("/gists/public", nil)
case .GetMyStarred:
return ("/gists/starred", nil)
case .GetAtPath(let path):
let URL = NSURL(string: path)
Authentication 108

let relativePath = URL!.relativePath!


return (relativePath, nil)
}
}()

let URL = NSURL(string: GistRouter.baseURLString)!


let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))

let encoding = Alamofire.ParameterEncoding.JSON


let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)

encodedRequest.HTTPMethod = method.rawValue

return encodedRequest
}
}

10.5.1 Get the OAuth Token Before Printing Gists


When the app starts up we’ll need to get an OAuth token, if we don’t already have one. So before
calling printMyStarredGistsWithOAuth2 we’ll need to check whether we already have an OAuth
token and get one if we don’t.
So in the MasterViewController, we’ll add a method to do the initial data loading. It’ll grab an OAuth
token if we need it and print the starred gists if we already have a token. Later we’ll be able to swap
out printMyStarredGistsWithOAuth2 to with our loadGists function but first we want to make
sure that OAuth works:

override func viewDidAppear(animated: Bool) {


super.viewDidAppear(animated)

loadInitialData()
}

func loadInitialData() {
if (!GitHubAPIManager.sharedInstance.hasOAuthToken()) {
showOAuthLoginView()
} else {
GitHubAPIManager.sharedInstance.printMyStarredGistsWithOAuth2()
}
}

It’ll be the GitHubAPIManager’s job to keep track of whether we have an OAuth token, so we’ll be
adding a method to check for one there: GitHubAPIManager.sharedInstance.hasOAuthToken().
Authentication 109

If we don’t have a token, then we’ll need to kick off the OAuth flow. We’ll do that by showing a view
that lets the user tap on a button to start logging in: showOAuthLoginView(). When they tap on the
login button we’ll call a function called URLToStartOAuth2Login() so the MasterViewController
can get the URL to start the login flow. We’ll need to create that view and implement those 2
functions.
And if we do already have a token, we can print the gists:

GitHubAPIManager.sharedInstance.printMyStarredGistsWithOAuth2()

This code needs a few more things to work:

1. Let us check if we have a token with hasOAuthToken


2. Create a login view
3. Start up the OAuth authorization flow with with startOAuth2Login
4. Make an authorized request and print out our starred gists once we have an OAuth token

We can fill in a structure for those items so we don’t forget about them later:

import Foundation
import Alamofire

class GitHubAPIManager
{
static let sharedInstance = GitHubAPIManager()

...

func hasOAuthToken() -> Bool {


// TODO: implement
return false
}

// MARK: - OAuth flow

func URLToStartOAuth2Login() -> NSURL? {


// TODO: implement
// TODO: get and print starred gists
}

func printMyStarredGistsWithOAuth2() -> Void {


alamofireManager.request(GistRouter.GetMyStarred())
.responseString { response in
Authentication 110

guard response.result.error == nil else {


print(response.result.error!)
return
}
if let receivedString = response.result.value {
print(receivedString)
}
}
}
}

10.5.2 Login View


It’s always best to keep the user aware of what’s happening. So instead of just kicking them over to
GitHub let’s pop up a view where they can confirm that they want to login.
Open up the storyboard and drag in a new view controller:

Add a button to the new view controller:


Authentication 111

Set its title to Login to GitHub:

Make it wide enough for the text:


Authentication 112

Select the button buttons and add a constraints to center it horizontally and vertically in the view:

To add some code to that button we need to create a new Swift file with a class to represent this
view controller. Create a new Swift file and name it LoginViewController.swift.
In your new login view controller code file we’ll need to add an IBAction to hook up to the button:
Authentication 113

import UIKit

class LoginViewController: UIViewController {

@IBAction func tappedLoginButton() {


// TODO: implement
}
}

Then we can switch back to the storyboard. Set the storyboard ID and the class of the new view
controller to LoginViewController:

And hook up the touch up inside event for the button to the IBAction that we just added to the code:
Authentication 114

Now we can set up the login view controller to be shown when they launch the app and they don’t
have an OAuth token yet. In MasterViewController:

func loadInitialData() {
if (!GitHubAPIManager.sharedInstance.hasOAuthToken()) {
showOAuthLoginView()
} else {
GitHubAPIManager.sharedInstance.printMyStarredGistsWithOAuth2()
}
}

func showOAuthLoginView() {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
if let loginVC = storyboard.instantiateViewControllerWithIdentifier(
"LoginViewController") as? LoginViewController {
self.presentViewController(loginVC, animated: true, completion: nil)
}
}

To display the view we need to get the storyboard to create an instance for us (using the storyboard
ID: LoginViewController). Then we can use the navigation controller that was created way back
when we first created this master-detail project to show push that view controller onto the view
stack.
So that will take care of showing the login view controller. But how does it go away? If they tap the
login button we should start the OAuth login flow and hide the login view. But our IBAction is in
the login view controller and we want to go back to the main view then start the OAuth process.
Fortunately there’s a common pattern used for this kind of code called delegation. We’ll define
a protocol that says what a delegate for the login view needs to do then we’ll just tell the
delegate to take action when the login button is pressed. We can add the protocol right in the
LoginViewController, we’ll call it LoginViewDelegate:
Authentication 115

import UIKit

protocol LoginViewDelegate: class {


func didTapLoginButton()
}

class LoginViewController: UIViewController {


weak var delegate: LoginViewDelegate?

@IBAction func tappedLoginButton() {


if let delegate = self.delegate {
delegate.didTapLoginButton()
}
}
}

So when the button is tapped we’ll just check that we have a delegate and, if we have one, we’ll tell
it what happened.

The delegate is declared as a weak var so that the login view controller won’t act like it
owns the delegate. Otherwise we could end up with a retain cycle where the delegate (our
MasterViewController) owns the LoginViewController and vice versa. If that happens then
neither view controller will ever be released and we’ll have a memory leak.
.

Guess we need to make the master view controller conform to that protocol so we can handle those
events:

class MasterViewController: UITableViewController, LoginViewDelegate {


...
}

And we’ll set it as the delegate just before we show the login view:
Authentication 116

func showOAuthLoginView() {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
if let loginVC = storyboard.instantiateViewControllerWithIdentifier(
"LoginViewController") as? LoginViewController {
loginVC.delegate = self
self.presentViewController(loginVC, animated: true, completion: nil)
}
}

And finally we’ll implement the protocol methods in the MasterViewController to handle tapping
the login button:

func didTapLoginButton() {
self.dismissViewControllerAnimated(false, completion: nil)

if let authURL = GitHubAPIManager.sharedInstance.URLToStartOAuth2Login() {


// TODO: show web page
}
}

When they tap the login button we’ll dismiss the login view and start the OAuth process.
We’ll be back to this code shortly after a quick trip to GitHub.

10.5.3 The OAuth Login Flow


To request a token from the GitHub API we can follow the flow in the docs¹³, even though it says
it’s for web apps:

1. Redirect users to request GitHub access


2. GitHub redirects back to your site (app for us) with a code
3. Exchange the code for an access token
4. Use the access token to access the API

Step 3 is the extra step I referred to at the start of this section. The user doesn’t see it happen so we
don’t always think of it as part of the OAuth 2.0 flow but as coders we need to implement it.
.

Step 1: Send Users to GitHub


The first thing we need to do is to send the user to a web page at GitHub. The endpoint we need to
send them to is:
¹³https://developer.github.com/v3/oauth/
Authentication 117

GET https://github.com/login/oauth/authorize

With a few parameters:

• client_id
• redirect_uri
• scope
• state

Only the client_id is required but we’ll provide everything except the redirect_uri since we can
specify that in the web interface.
To get a client ID head over to GitHub: Create a new OAuth app¹⁴
If you don’t have a GitHub account you’ll need to create a free one. You’ll also need to star some gists
so you can retrieve them in your API call.
Fill out the form. For the Authorization callback URL (which is the same thing as the redirect_uri
param), make up a URL format for your app that starts with some kind of unique ID for your app.
For example, I’m using grokGitHub://?aParam=paramVal with grokGitHub:// as the custom URL
protocol. The ?aParam=paramVal part isn’t necessary for our code but GitHub wouldn’t accept a
callback URL without some kind of parameters.
The Authorization callback URL will be used in step 2 when GitHub sends the user back to our app.
For step 1 we just need to copy the client_id from GitHub. We’ll need the client_secret later so we’ll
copy that too:

class GitHubAPIManager
{
static let sharedInstance = GitHubAPIManager()
var alamofireManager:Alamofire.Manager

let clientID: String = "1234567890"


let clientSecret: String = "abcdefghijkl"
...
}

Ideally we wouldn’t store the client ID & secret in our app since a malicious person could extract
it from there. But it greatly simplifies showing how to implement OAuth and this section is plenty
long enough as it is.
So now that we’ve got our client ID (and had a nice little break, seriously I did, you totally should),
we can implement URLToStartOAuth2Login():
¹⁴https://github.com/settings/applications/new
Authentication 118

// MARK: - OAuth flow


func URLToStartOAuth2Login() -> NSURL? {
let authPath:String = "https://github.com/login/oauth/authorize" +
"?client_id=\(clientID)&scope=gist&state=TEST_STATE"
guard let authURL:NSURL = NSURL(string: authPath) else {
// TODO: handle error
return nil
}

return authURL
}

And in didTapLoginButton() we’ll use that function to send the user to that web page. iOS 9 has a
nice new class SFSafariViewController that we can use to send the user to the OAuth login web
page.
To use the SFSafariViewController we need add the Safari Services framework to our project. To
do that, click on your project in the organizer (top left corner). Then select your target and scroll all
the way down in the first panel until you find the Linked Frameworks and Files Section. Click the
plus sign under that section:

Linked Frameworks and Files Section

Then select the Safari Services Framework:


Authentication 119

Adding the Framework

And you should see that it’s been added to your project:

Framework Added

Now we can import the framework. We’ll need to keep that view controller around as a variable (and
make MasterViewController its delegate). That’ll let us show the web page, handle it not loading if
they don’t have an internet connection, and hide it when they’re done with it.
Authentication 120

import SafariServices

class MasterViewController: UITableViewController, LoginViewDelegate,


SFSafariViewControllerDelegate {
var safariViewController: SFSafariViewController?
...
}

Then we can create the view controller and display it:

func didTapLoginButton() {
self.dismissViewControllerAnimated(false, completion: nil)

if let authURL = GitHubAPIManager.sharedInstance.URLToStartOAuth2Login() {


safariViewController = SFSafariViewController(URL: authURL)
safariViewController?.delegate = self
if let webViewController = safariViewController {
self.presentViewController(webViewController, animated: true, completion: nil)
}
}
}

And we’ll make sure the web page loads, dismissing the view if it fails:

// MARK: - Safari View Controller Delegate


func safariViewController(controller: SFSafariViewController, didCompleteInitialLoad
didLoadSuccessfully: Bool) {
// Detect not being able to load the OAuth URL
if (!didLoadSuccessfully) {
// TODO: handle this better
controller.dismissViewControllerAnimated(true, completion: nil)
}
}

Later we’ll add dismissing the view if it loads correctly and they login.
Here’s what it looks like when UIApplication.sharedApplication().openURL(authURL) sends the
user to Safari so they can authorize our app for their GitHub account:
Authentication 121

Authorize App with GitHub OAuth

So that takes care of step 1. But if we click on the Authorize button on that web page we’ll get an
error:

Invalid Address Error

That’s because GitHub is trying to send the user back to our app using the callback URL that we
provided: grokGitHub://?aParam=paramVal. But iOS has no idea what to do with a grokGitHub://
URL. So we need to tell iOS that our app will handle grokGitHub:// URLs.
Step 2: GitHub Redirects Back
In iOS any app can register a URL scheme. That’s what we’ll use to tell the operating system that
our app will handle grokGitHub:// URLs. Then GitHub will be able to send the user back to our
app along with the authorization code that we’ll later exchange for a token.

Why do we get a code to exchange for a token instead of just getting the token? Did you notice the
state parameter in step 1? That’s for our security, if we want to implement it. We can send a state
parameter and then make sure we get it back. If we don’t get it back then we can just not finish
.
Authentication 122

the OAuth flow and a token doesn’t get generated. That way we can be sure that step 2 is getting
fired off by our app, not some random person or bot trying to get access to our GitHub account.
.

To register a custom URL scheme, open up the info.plist in your Xcode project:

Open Info.plist

Right click on the info.plist and select “Add Row”:

Add Row to Info.plist

Change the identifier to “URL types”:


Authentication 123

Select URL Types

It should change the type to Array and add a sub-row “Item 0” with a “URL identifier” in it:

URL Types Array

The URL identifier should be unique. The easiest thing to use is your app ID:

Set URL ID

And right-click on Item 0 to add another row under it. Make that row “URL Schemes”:

Add URL Schemes

Set the URL Schemes’s Item 0 to your custom URL scheme without the :// (set it to grokGitHub, not
grokGitHub://):
Authentication 124

Final URL Scheme Settings

Then switch to the AppDelegate file and add an application:handleOpenURL: function to indicate
that we can open URLs (you can delete most of the boilerplate that Xcode generated to leave just
this stuff):

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
var window: UIWindow?

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions:


[NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let splitViewController = self.window!.rootViewController as! UISplitViewController
let navigationController = splitViewController.viewControllers[
splitViewController.viewControllers.count-1] as! UINavigationController
navigationController.topViewController!.navigationItem.leftBarButtonItem
= splitViewController.displayModeButtonItem()
splitViewController.delegate = self
return true
}

func application(application: UIApplication, handleOpenURL url: NSURL) -> Bool {


return true
}

// MARK: - Split view

func splitViewController(splitViewController: UISplitViewController,


collapseSecondaryViewController secondaryViewController:UIViewController,
ontoPrimaryViewController primaryViewController:UIViewController) -> Bool {
guard let secondaryAsNavController = secondaryViewController as?
UINavigationController else { return false }
guard let topAsDetailController = secondaryAsNavController.topViewController as?
DetailViewController else { return false }
if topAsDetailController.detailItem == nil {
// Return true to indicate that we have handled the collapse by doing nothing
Authentication 125

// the secondary controller will be discarded.


return true
}
return false
}
}

That should be all you need for the custom URL scheme. To test it, launch your app. The code that
we set up for OAuth step 1 above should send you to Safari then back to our app. If you’re having
issues with it, revoke the GitHub access for your app on the Authorized Applications tab¹⁵ so you
can re-authorize it. We’ll stop it from going to Safari every time you launch later but for now it’s
handy to make sure the custom URL scheme is working.
Step 3: Swap the Code for a Token
When GitHub calls our custom URL scheme it passes us a code. We’ll need to process the URL that
we got to extract that code and then exchange it for an OAuth token. First we need to send the URL
that was used to open the app over to our GitHubAPIManager since it’s responsible for that kinda
stuff. So change the function in the app delegate to:

func application(application: UIApplication, handleOpenURL url: NSURL) -> Bool {


GitHubAPIManager.sharedInstance.processOAuthStep1Response(url)
return true
}

And flip over to the GitHubAPIManager file to implement processOAuthStep1Response:

class GitHubAPIManager
{
static let sharedInstance = GitHubAPIManager()

...

func processOAuthStep1Response(url: NSURL)


{
// TODO: implement
}
}

The URL we’re receiving looks like:

¹⁵https://github.com/settings/applications
Authentication 126

grokgithub://?aParam=paramVal&code=123456789&state=TEST_STATE

Don’t believe me? Add print(url) in processOAuthStep1Response to check for yourself.


We need to process that URL to extract the argument after &code=. Fortunately iOS has tools to
process URL components as query items with names and values:

func processOAuthStep1Response(url: NSURL) {


let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)
var code:String?
if let queryItems = components?.queryItems {
for queryItem in queryItems {
if (queryItem.name.lowercaseString == "code") {
code = queryItem.value
break
}
}
}
}

So we can turn the URL into an array of queryItems (which each have a name and a value), then
go through those by name until we find the code item, then grab its value.
If we get a code, we can set up the Alamofire request to exchange it for an OAuth token. Checking
the GitHub docs¹⁶ we can see that we need to make a POST request to:

https://github.com/login/oauth/access_token

With our client ID, client secret and the code we just received as parameters. We’ll also use a header
to specify that we want the response as JSON:

if let receivedCode = code {


let getTokenPath:String = "https://github.com/login/oauth/access_token"
let tokenParams = ["client_id": clientID, "client_secret": clientSecret,
"code": receivedCode]
let jsonHeader = ["Accept": "application/json"]
Alamofire.request(.POST, getTokenPath, parameters: tokenParams,
headers: jsonHeader)
.responseString { response in
// TODO: handle response to extract OAuth token
}
}

Once we have that response, we can check for errors (kicking out if we’ve got one) and see what the
results look like to figure out how to parse out the OAuth token (assuming there wasn’t an error):
¹⁶https://developer.github.com/v3/oauth/#github-redirects-back-to-your-site
Authentication 127

if let error = response.result.error {


print(rror)
return
}
print(response.result.value)
// like "access_token=999999&scope=gist&token_type=bearer"

If we get an OAuth token we’ll need to store it. For now we’ll just stick it in a variable on our
GitHubAPIManager. A little later we’ll figure out how to persist it between runs of the app and store
it securely:

class GitHubAPIManager
{
static let sharedInstance = GitHubAPIManager()

var OAuthToken: String?

...
}

To parse out the OAuth token, we’ll step through the parameters in the results:

if let receivedResults = response.result.value, jsonData =


receivedResults.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) {
let jsonResults = JSON(data: jsonData)
for (key, value) in jsonResults {
switch key {
case "access_token":
self.OAuthToken = value.string
case "scope":
// TODO: verify scope
print("SET SCOPE")
case "token_type":
// TODO: verify is bearer
print("CHECK IF BEARER")
default:
print("got more than I expected from the OAuth token exchange")
print(key)
}
}
}

After we’ve converted the results to JSON, we loop through the key-value pairs, we check each key
and figure out what to do with it. To keep things simple, I’ve just tossed in a TODO for each key
Authentication 128

that we don’t need right now. If you were really deploying this code in an app you’d want to make
sure that you get the right type of token (bearer) and have the right kind of scope (gists).
Ok, so we’ve got the OAuth token saved (if we got one):

self.OAuthToken = value

Now we need to use it to get our starred gists, let’s add that after getting the token:

if let receivedCode = code {


let getTokenPath:String = "https://github.com/login/oauth/access_token"
let tokenParams = ["client_id": clientID, "client_secret": clientSecret,
"code": receivedCode]
let jsonHeader = ["Accept": "application/json"]
Alamofire.request(.POST, getTokenPath, parameters: tokenParams,
headers: jsonHeader)
.responseString { response in
if let error = response.result.error {
print(error)
return
}
print(response.result.value)
if let receivedResults = response.result.value, ... {
...
}
if (self.hasOAuthToken()) {
self.printMyStarredGistsWithOAuth2()
}
}

See any issues with that code? We didn’t update self.hasOAuthToken() to reflect whether we
actually have a token! Better do that or we’ll always get an error returned:

func hasOAuthToken() -> Bool {


if let token = self.OAuthToken {
return !token.isEmpty
}
return false
}

Now hasOAuthToken() is true if we have a token and it’s not blank.


Authentication 129

10.5.4 Handling Multiple Launches


So what happens if we run the app now? Well, every time the MasterViewController gets shown
we:

• Check for an OAuth token


• If we don’t have one, we show the login view
• If we do have one, we try to fetch our starred gists

But the MasterViewController gets shown each time we launch the app, including when Safari re-
opens the app using our custom URL scheme. That’s a problem since at that point we’ll only have a
code, not a token! So the login view will get shown again.
To get around that we can check if we’ve already started the OAuth process. So when we start the
OAuth process we’ll save a bool to the NSUserDefaults that says we’re currently loading the OAuth
token:

func didTapLoginButton() {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(true, forKey: "loadingOAuthToken")

self.dismissViewControllerAnimated(false, completion: nil)

if let authURL = GitHubAPIManager.sharedInstance.URLToStartOAuth2Login() {


safariViewController = SFSafariViewController(URL: authURL)
safariViewController?.delegate = self
if let webViewController = safariViewController {
self.presentViewController(webViewController, animated: true, completion: nil)
}
}
}

And we’ll set it to false when we’ve got an OAuth token (or we’ve failed to do so). We need to do
that if we got a URL without a code in it, if we get an error from the POST request, or after we’ve
parsed the response to trade a code for a token:
Authentication 130

func processOAuthStep1Response(url: NSURL) {


let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)
var code:String?
if let queryItems = components?.queryItems {
for queryItem in queryItems {
if (queryItem.name.lowercaseString == "code") {
code = queryItem.value
break
}
}
}
if let receivedCode = code {
let getTokenPath:String = "https://github.com/login/oauth/access_token"
let tokenParams = ["client_id": clientID, "client_secret": clientSecret,
"code": receivedCode]
let jsonHeader = ["Accept": "application/json"]
Alamofire.request(.POST, getTokenPath, parameters: tokenParams, headers: jsonHeader)
.responseString { response in
if let error = response.result.error {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")
// TODO: bubble up error
return
}
print(response.result.value)
if let receivedResults = response.result.value, jsonData =
receivedResults.dataUsingEncoding(NSUTF8StringEncoding,
allowLossyConversion: false) {
let jsonResults = JSON(data: jsonData)
for (key, value) in jsonResults {
switch key {
case "access_token":
self.OAuthToken = value.string
case "scope":
// TODO: verify scope
print("SET SCOPE")
case "token_type":
// TODO: verify is bearer
print("CHECK IF BEARER")
default:
print("got more than I expected from the OAuth token exchange")
print(key)
}
}
}
Authentication 131

if (self.hasOAuthToken()) {
self.printMyStarredGistsWithOAuth2()
}
}
}
}

That’s getting pretty long, let’s break out the section that makes the call to swap the code for a token
into its own function:

func swapAuthCodeForToken(receivedCode: String) {


let getTokenPath:String = "https://github.com/login/oauth/access_token"
let tokenParams = ["client_id": clientID, "client_secret": clientSecret,
"code": receivedCode]
let jsonHeader = ["Accept": "application/json"]
Alamofire.request(.POST, getTokenPath, parameters: tokenParams, headers: jsonHeader)
.responseString { response in
if let error = response.result.error {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")
// TODO: bubble up error
return
}
print(response.result.value)
if let receivedResults = response.result.value, jsonData =
receivedResults.dataUsingEncoding(NSUTF8StringEncoding,
allowLossyConversion: false) {

let jsonResults = JSON(data: jsonData)


for (key, value) in jsonResults {
switch key {
case "access_token":
self.OAuthToken = value.string
case "scope":
// TODO: verify scope
print("SET SCOPE")
case "token_type":
// TODO: verify is bearer
print("CHECK IF BEARER")
default:
print("got more than I expected from the OAuth token exchange")
print(key)
}
}
}
Authentication 132

let defaults = NSUserDefaults.standardUserDefaults()


defaults.setBool(false, forKey: "loadingOAuthToken")
if (self.hasOAuthToken()) {
self.printMyStarredGistsWithOAuth2()
}
}
}

func processOAuthStep1Response(url: NSURL) {


let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)
var code:String?
if let queryItems = components?.queryItems {
for queryItem in queryItems {
if (queryItem.name.lowercaseString == "code") {
code = queryItem.value
break
}
}
}
if let receivedCode = code {
swapAuthCodeForToken(receivedCode)
} else {
// no code in URL that we launched with
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")
}
}

Then we can change the MasterViewController to check whether we’re loading the OAuth token
before we start loading data or start the OAuth login process:

override func viewDidAppear(animated: Bool) {


super.viewDidAppear(animated)

let defaults = NSUserDefaults.standardUserDefaults()


if (!defaults.boolForKey("loadingOAuthToken")) {
loadInitialData()
}
}

And we’ll need to update it if we can’t load the OAuth web page:
Authentication 133

func safariViewController(controller: SFSafariViewController, didCompleteInitialLoad


didLoadSuccessfully: Bool) {
// Detect not being able to load the OAuth URL
if (!didLoadSuccessfully) {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")
controller.dismissViewControllerAnimated(true, completion: nil)
}
}

NSUserDefaults is a dictionary that’s persisted between runs of the app. It’s a good place to store
small bits of data that need to be kept around but don’t need to be secure.

10.5.5 Using the OAuth Token for API Calls


Ok, so we’ve finally got a token, now what do we do with it? We’ve got to pass it in an Authorization
header with each call to the GitHub API.
With our Alamofire Router it’s easy to include this header in all of the API calls. We’ll just add it to
the NSMutableURL before we return it:

enum GistRouter: URLRequestConvertible {


...

var URLRequest: NSMutableURLRequest {


...

let URL = NSURL(string: GistRouter.baseURLString)!


let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))

// Set OAuth token if we have one


if let token = GitHubAPIManager.sharedInstance.OAuthToken {
URLRequest.setValue("token \(token)", forHTTPHeaderField: "Authorization")
}

let encoding = Alamofire.ParameterEncoding.JSON


let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)

encodedRequest.HTTPMethod = method.rawValue

return encodedRequest
}
}
Authentication 134

But there’s still a problem: the OAuthToken isn’t getting stored between runs of the app so we’re
getting prompted to login every time we launch the app. We’ll want to store it securely so we won’t
just use NSUserDefaults.

10.5.6 Storing the OAuth Token Securely


The place to save most secure data in an iOS app is the Keychain. The code to work with the keychain
can be pretty ugly so we’ll use a nice library called Locksmith¹⁷ to provide a simpler interface.
Add Locksmith v2.0 to your project using CocoaPods.
When that’s done switch back to Xcode. At the top of the GitHubAPIManager file add import
Locksmith:

import Foundation
import Alamofire
import Locksmith

class GitHubAPIManager
{
...
}

Now using Locksmith we can save & retrieve the OAuth token:

var OAuthToken: String? {


set {
if let valueToSave = newValue {
do {
try Locksmith.updateData(["token": valueToSave], forUserAccount: "github")
} catch {
let _ = try? Locksmith.deleteDataForUserAccount("github")
}
}
else { // they set it to nil, so delete it
let _ = try? Locksmith.deleteDataForUserAccount("github")
}
}
get {
// try to load from keychain
Locksmith.loadDataForUserAccount("github")
let dictionary = Locksmith.loadDataForUserAccount("github")
if let token = dictionary?["token"] as? String {

¹⁷https://github.com/matthewpalmer/Locksmith
Authentication 135

return token
}
return nil
}
}

There are a few things in that code that deserve some explanation:
newValue is what Swift passes in to a getter to tell us what the user is trying to set it to. So if we had
GitHubManager.sharedInstance().OAuthToken = "abcd1234". The newValue within the set block
for OAuthToken would be "abcd1234".
We’re using Locksmith.updateData because it will save the value even if we already have one saved
in the Keychain. If we used Locksmith.saveData instead it would throw an error if we already had
a value saved, which isn’t what we want.
Swift 2.0 introduced do-try-catch. Since the Locksmith statements are marked throws we need to
allow for the possibility of them throwing an exception. Sometimes we want to take special action,
like if we can’t save the value we should make sure we don’t have an old value saved:

do {
try Locksmith.updateData(["token": valueToSave], forUserAccount: "github")
} catch {
// Handle exception
}

And sometimes we want to just perform the action without worrying about the exception:

let _ = try? Locksmith.deleteDataForUserAccount("github")

10.5.7 Making Authenticated Calls


Ok, so the GitHubAPIManager looks good, how do we use it? Well, earlier we set up a printMyS-
tarredGistsWithOAuth2 function that looked like this:
Authentication 136

func printMyStarredGistsWithOAuth2() -> Void {


alamofireManager.request(GistRouter.GetMyStarred())
.responseString { response in
guard response.result.error == nil else {
print(response.result.error!)
return
}
if let receivedString = response.result.value {
print(receivedString)
}
}
}

Since we’re already using the router our OAuth token should get automatically added when we call
GistRouter.GetMyStarred(). Save and try it out.
All that work earlier pays off now to make that nice & simple. It’ll be easy to extend this OAuth
token handling as we add more API calls, as long as we use our router (and we requested the correct
scope when we got an OAuth token).

Set up your project to make authenticated API calls.

10.5.8 And That’s the Login Flow for OAuth 2.0


I know, that’s a lot to process. If you have issues while testing, the first thing to try is revoking access
so the OAuth process can start fresh. For GitHub you can do that on the Authorized Applications
tab¹⁸. You might also want to wipe out the OAuth token if the printMyStarredGistsWithOAuth2 call
fails:

func printMyStarredGistsWithOAuth2() -> Void {


alamofireManager.request(.GET, "https://api.github.com/gists/starred")
.responseString { _, _, result in
guard result.error == nil else {
print(result.error)
GitHubAPIManager.sharedInstance.OAuthToken = nil
return
}
if let receivedString = result.value {
print(receivedString)
}
}
}

¹⁸https://github.com/settings/applications
Authentication 137

If you’re getting errors about the authorization credentials not being correct you can print the request
(which includes the headers) using debugPrint to make sure they’re correct:

func printMyStarredGistsWithOAuth2() -> Void {


let starredGistsRequest = alamofireManager.request(GistRouter.GetMyStarred())
.responseString { _, _, result in
guard result.error == nil else {
print(result.error)
GitHubAPIManager.sharedInstance.OAuthToken = nil
return
}
if let receivedString = result.value {
print(receivedString)
}
}
debugPrint(starredGistsRequest)
}

If all else fails, there’s a “Reset Content and Settings” option in the iOS Simulator menu that’ll get
you back to a vanilla state. You’ll probably find you need to do all 3 of those things sometimes while
debugging OAuth but just until you get this token stuff working. Then you’ll never have to look at
it again.

10.6 Displaying the Results


So how about displaying those gists in the table view? We already have it set up for showing public
gists, let’s switch it to showing starred gists.
Here’s the code we have for getting public gists (including the pagination and pull to refresh features
that we added in previous chapters):

func getGists(urlRequest: URLRequestConvertible, completionHandler:


(Result<[Gist], NSError>, String?) -> Void) {
alamofireManager.request(urlRequest)
.validate()
.responseArray { (response:Response<[Gist], NSError>) in
guard response.result.error == nil,
let gists = response.result.value else {
print(response.result.error)
completionHandler(response.result, nil)
return
}
Authentication 138

// need to figure out if this is the last page


// check the link header, if present
let next = self.getNextPageFromHeaders(response.response)
completionHandler(.Success(gists), next)
}
}

func getPublicGists(pageToLoad: String?, completionHandler:


(Result<[Gist], NSError>, String?) -> Void) {
if let urlString = pageToLoad {
getGists(GistRouter.GetAtPath(urlString), completionHandler: completionHandler)
} else {
getGists(GistRouter.GetPublic(), completionHandler: completionHandler)
}
}

We’ll need to add a getMyStarredGists function:

func getMyStarredGists(pageToLoad: String?, completionHandler:


(Result<[Gist], NSError>, String?) -> Void) {
if let urlString = pageToLoad {
getGists(GistRouter.GetAtPath(urlString), completionHandler: completionHandler)
} else {
getGists(GistRouter.GetMyStarred(), completionHandler: completionHandler)
}
}

Now we’d like to use one of these functions in loadInitialData, just like we did with getPub-
licGists but there’s a problem. What if we have to get an OAuth token first? We don’t want to
freeze up the app while we’re getting the token so we’re using an asynchronous implementation.
But then how will we know when it’s done?
Normally we could add a completion handler to the method call, like we do in getGists. That would
let us add a block of code to be called when the method is done. The problem is that part of getting
the OAuth token is going to be the URL handling for our custom URL scheme, which doesn’t happen
in the functions we’re calling directly. It’s really, really asynchronous. What we can do instead of
passing a block of code to the startOAuth2Login method (that’ll get forgotten we get kicked to
Safari) is to give the block of code to the GitHubAPIManager. Then the GitHubAPIManager can hold
on to that code block until we’ve received an OAuth token.
It’ll look like this:
Authentication 139

GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler = {
// code that we want to execute when we get an OAuth token
}

More specifically, we’ll want to check for any errors then fetch the repos if there are no errors:

GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler = { (error) -> Void in


if let error = error {
print(error)
self.isLoading = false
// TODO: handle error
// Something went wrong, try again
self.showOAuthLoginView()
} else {
self.loadGists(nil)
}
}

And naturally that’ll only work if we add the variable to the GitHubAPIManager class:

class GitHubAPIManager
{
static let sharedInstance = GitHubAPIManager()

// handlers for the OAuth process


// stored as vars since sometimes it requires a round trip to safari which
// makes it hard to just keep a reference to it
var OAuthTokenCompletionHandler:(NSError? -> Void)?

...
}

Once that completion block is set up we can use it in loadInitialData(). We’ll set it then either
start the OAuth login process or load the gists if we already have a token. Once we get an OAuth
token then we’ll load the gists:
Authentication 140

func loadInitialData() {
isLoading = true
GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler = { (error) -> Void in
self.safariViewController?.dismissViewControllerAnimated(true, completion: nil)
if let error = error {
print(error)
self.isLoading = false
// TODO: handle error
// Something went wrong, try again
self.showOAuthLoginView()
} else {
self.loadGists(nil)
}
}

if (!GitHubAPIManager.sharedInstance.hasOAuthToken()) {
self.showOAuthLoginView()
} else {
loadGists(nil)
}
}

From our pagination and pull to refresh work we have an implementation of loadGists that looks
like this:

func loadGists(urlToLoad: String?) {


self.isLoading = true
GitHubAPIManager.sharedInstance.getPublicGists(urlToLoad) { (result, nextPage) in
self.isLoading = false
self.nextPageURLString = nextPage
// tell refresh control it can stop showing up now
if self.refreshControl != nil && self.refreshControl!.refreshing {
self.refreshControl?.endRefreshing()
}

guard result.error == nil else {


print(result.error)
self.nextPageURLString = nil
return
}

if let fetchedGists = result.value {


if urlToLoad != nil {
self.gists += fetchedGists
} else {
Authentication 141

self.gists = fetchedGists
}
}

// 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)

self.tableView.reloadData()
}
}

All we need to do is switch it to use getMyStarredGists instead of getPublicGists:

func loadGists(urlToLoad: String?) {


self.isLoading = true
GitHubAPIManager.sharedInstance.getMyStarredGists(urlToLoad) { (result, nextPage) in
self.isLoading = false
self.nextPageURLString = nextPage
// tell refresh control it can stop showing up now
if self.refreshControl != nil && self.refreshControl!.refreshing {
self.refreshControl?.endRefreshing()
}

guard result.error == nil else {


print(result.error)
self.nextPageURLString = nil
return
}

if let fetchedGists = result.value {


if urlToLoad != nil {
self.gists += fetchedGists
} else {
self.gists = fetchedGists
}
}

// 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)

self.tableView.reloadData()
Authentication 142

}
}

And we need to update the pull to refresh function to call the loadInitialData function so that if
there’s an issue authenticating the user can try again by pulling to refresh:

func refresh(sender:AnyObject) {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")

nextPageURLString = nil // so it doesn't try to append the results


loadInitialData()
}

Now that we’ve implemented loadGists let’s fix the OAuth process so that the completion handler
gets called right after we get an OAuth token. Otherwise the user would have to pull to refresh to
get the list of gists to load.
We’ll need to read through the login flow for OAuth 2.0 and find each spot where an error could
bubble up. We’ll need to call the completion handler for those errors and when we finally get the
token.
Starting in didTapLoginButton, we could have a problem if the authorize URL isn’t valid. We could
find that out two ways: the authURL can’t be created by the NSURL initializer or the web page doesn’t
load. So let’s handle both of those cases (in GitHubAPIManager):

func didTapLoginButton() {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(true, forKey: "loadingOAuthToken")

self.dismissViewControllerAnimated(false, completion: nil)

if let authURL = GitHubAPIManager.sharedInstance.URLToStartOAuth2Login() {


safariViewController = SFSafariViewController(URL: authURL)
safariViewController?.delegate = self
if let webViewController = safariViewController {
self.presentViewController(webViewController, animated: true, completion: nil)
}
} else {
defaults.setBool(false, forKey: "loadingOAuthToken")
if let completionHandler =
GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler {
let error = NSError(domain: GitHubAPIManager.ErrorDomain, code: -1,
userInfo: [NSLocalizedDescriptionKey:
Authentication 143

"Could not create an OAuth authorization URL",


NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(error)
}
}
}

func safariViewController(controller: SFSafariViewController, didCompleteInitialLoad


didLoadSuccessfully: Bool) {
// Detect not being able to load the OAuth URL
if (!didLoadSuccessfully) {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")
if let completionHandler =
GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler {
let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet,
userInfo: [NSLocalizedDescriptionKey: "No Internet Connection",
NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(error)
}
controller.dismissViewControllerAnimated(true, completion: nil)
}
}

In both cases, if there’s a problem with the URL then we need to call the completion handler and
reset the “loadingAuthToken” state to false.
We’ll have to declare that custom error domain for the first case:

class GitHubAPIManager {
...
static let ErrorDomain = "com.error.GitHubAPIManager"
...
}

Then next step in the process is in handleOpenURL in the AppDelegate:

func application(application: UIApplication, handleOpenURL url: NSURL) -> Bool {


GitHubAPIManager.sharedInstance.processOAuthStep1Response(url)
return true
}

Doesn’t look like there’s anything to go wrong there.


Next, processOAuthStep1Response which currently looks like this:
Authentication 144

func processOAuthStep1Response(url: NSURL) {


let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)
var code:String?
if let queryItems = components?.queryItems {
for queryItem in queryItems {
if (queryItem.name.lowercaseString == "code") {
code = queryItem.value
break
}
}
}
if let receivedCode = code {
swapAuthCodeForToken(receivedCode)
}
}

If this function can’t find the code in the queryItems then it needs to let the completion handler
know that it failed. We’ll make a custom error for that:

func processOAuthStep1Response(url: NSURL) {


let components = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)
var code:String?
if let queryItems = components?.queryItems {
for queryItem in queryItems {
if (queryItem.name.lowercaseString == "code") {
code = queryItem.value
break
}
}
}
if let receivedCode = code {
swapAuthCodeForToken(receivedCode)
} else {
// no code in URL that we launched with
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")

if let completionHandler = self.OAuthTokenCompletionHandler {


let noCodeInResponseError = NSError(domain: GitHubAPIManager.ErrorDomain, code: -1,
userInfo: [NSLocalizedDescriptionKey: "Could not obtain an OAuth code",
NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(noCodeInResponseError)
}
}
}
Authentication 145

Ok, one more step in the OAuth process, swapAuthCodeForToken:

func swapAuthCodeForToken(receivedCode: String) {


let getTokenPath:String = "https://github.com/login/oauth/access_token"
let tokenParams = ["client_id": clientID, "client_secret": clientSecret,
"code": receivedCode]
let jsonHeader = ["Accept": "application/json"]
Alamofire.request(.POST, getTokenPath, parameters: tokenParams, headers: jsonHeader)
.responseString { response in
if let error = response.result.error {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")
// TODO: bubble up error
return
}
print(response.result.value)
if let receivedResults = response.result.value, jsonData =
receivedResults.dataUsingEncoding(NSUTF8StringEncoding,
allowLossyConversion: false) {

let jsonResults = JSON(data: jsonData)


for (key, value) in jsonResults {
switch key {
case "access_token":
self.OAuthToken = value.string
case "scope":
// TODO: verify scope
print("SET SCOPE")
case "token_type":
// TODO: verify is bearer
print("CHECK IF BEARER")
default:
print("got more than I expected from the OAuth token exchange")
print(key)
}
}
}

let defaults = NSUserDefaults.standardUserDefaults()


defaults.setBool(false, forKey: "loadingOAuthToken")
if (self.hasOAuthToken()) {
self.printMyStarredGistsWithOAuth2()
}
}
}
Authentication 146

We have success and failure cases here. Currently when we get the OAuth token we’re printing the
starred gists. Instead we’ll call the completion handler without an error to let it know it can load the
gists and display them now. We’ll also let it know if we fail:

let defaults = NSUserDefaults.standardUserDefaults()


defaults.setBool(false, forKey: "loadingOAuthToken")

if let completionHandler = self.OAuthTokenCompletionHandler {


if (self.hasOAuthToken()) {
completionHandler(nil)
} else {
let noOAuthError = NSError(domain: GitHubAPIManager.ErrorDomain, code: -1, userInfo:
[NSLocalizedDescriptionKey: "Could not obtain an OAuth token",
NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(noOAuthError)
}
}

There’s also one more error path. Right after we make the URL request we check for any errors and
return if we find one. We should call the completion handler there too:

Alamofire.request(.POST, getTokenPath, parameters: tokenParams, headers: jsonHeader)


.responseString { response in
if let error = response.result.error {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")

if let completionHandler = self.OAuthTokenCompletionHandler {


completionHandler(error)
}
return
}
...

Here’s the fully updated swapAuthCodeForToken function:


Authentication 147

func swapAuthCodeForToken(receivedCode: String) {


let getTokenPath:String = "https://github.com/login/oauth/access_token"
let tokenParams = ["client_id": clientID, "client_secret": clientSecret,
"code": receivedCode]
let jsonHeader = ["Accept": "application/json"]
Alamofire.request(.POST, getTokenPath, parameters: tokenParams, headers: jsonHeader)
.responseString { response in
if let error = response.result.error {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")

if let completionHandler = self.OAuthTokenCompletionHandler {


completionHandler(error)
}
return
}
print(response.result.value)
if let receivedResults = response.result.value, jsonData =
receivedResults.dataUsingEncoding(NSUTF8StringEncoding,
allowLossyConversion: false) {
let jsonResults = JSON(data: jsonData)
for (key, value) in jsonResults {
switch key {
case "access_token":
self.OAuthToken = value.string
case "scope":
// TODO: verify scope
print("SET SCOPE")
case "token_type":
// TODO: verify is bearer
print("CHECK IF BEARER")
default:
print("got more than I expected from the OAuth token exchange")
print(key)
}
}
}

let defaults = NSUserDefaults.standardUserDefaults()


defaults.setBool(false, forKey: "loadingOAuthToken")

if let completionHandler = self.OAuthTokenCompletionHandler {


if (self.hasOAuthToken()) {
completionHandler(nil)
} else {
Authentication 148

let noOAuthError = NSError(domain: GitHubAPIManager.ErrorDomain, code: -1,


userInfo: [NSLocalizedDescriptionKey: "Could not obtain an OAuth token",
NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(noOAuthError)
}
}
}
}

There we go, our super-async OAuthTokenCompletionHandler is now fully integrated with our table
view and GitHubAPIManager. We’ve built a table view that displays data from an OAuth 2.0 API.
Save and run to test it out. Try revoking the OAuth access to your GitHub account and deleting the
app to force it to run as if it’s the very first run of the app.

Finish integrating your authenticated API calls with your table view.

Here’s the finished code on GitHub (tagged “oauth”)¹⁹.

10.6.1 Refresh Tokens


Some OAuth implementations will give you an access token that expires along with a refresh token
that you can use to get a new access token later (along with your client ID and client secret).
GitHub doesn’t do that. If you’re working with a different OAuth API then you should check the
documentation to see if it returns a refresh token. If so, store the refresh token along with access
token so you can use it later.
Refreshing a token is similar to when we exchanged the code from the URL for an OAuth token:
Make a GET request providing the refresh token with your client ID and secret.
The advantage of using refresh tokens is that a compromised access token can only be used for a set
time period. A refresh token is useless without the client ID and secret, so it would be best to secure
those as well, but this section is long enough without adding something like creating a web server
to host those values.

Check your API documentation to see if you need to handle refresh tokens. If so, save it in
the Keychain like the OAuth token. Then add an function to check for the expiry, similar
to the unauthorized handling function. When you need to refresh the token you’ll need to
follow a process similar to when we traded the OAuth code for the token.

¹⁹https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/oauth
Authentication 149

10.7 Unauthorized Responses: 404 vs 401


What if the user later revokes our access? Then we’d have to do the OAuth login process again. What
would happen in our code if we don’t have a valid OAuth token? It’s easy to check, just revoke it
ourselves²⁰.
The next time we make an API call we’ll run into trouble. We’ll think we have a valid OAuth token
but it won’t be valid, so the call will fail. And we’ll just get a generic error:

Error Domain=com.alamofire.error Code=-1 "The operation couldn't be completed. (com.alamof\


ire.error error -1.)"

But we if take a look at the response to our Alamofire request we can see that the HTTP status code
is 401 Unauthorized. So we know that our token isn’t valid anymore.
We’ll add a new function that will return an error if we’re unauthorized:

func checkUnauthorized(urlResponse: NSHTTPURLResponse) -> (NSError?) {


if (urlResponse.statusCode == 401) {
self.OAuthToken = nil
let lostOAuthError = NSError(domain: NSURLErrorDomain,
code: NSURLErrorUserAuthenticationRequired,
userInfo: [NSLocalizedDescriptionKey: "Not Logged In",
NSLocalizedRecoverySuggestionErrorKey: "Please re-enter your GitHub credentials"])
return lostOAuthError
}
return nil
}

This function takes in the NSHTTPURLResponse that we can access in our response serializers and
checks the HTTP status code. If it’s 401 Unauthorized, then we generate an error and return it.
Otherwise it just returns nil. Since there’s already a relevant error code available in iOS called
NSURLErrorUserAuthenticationRequired in NSURLErrorDomain so we’ll use that for our error code.

Then we can use that function in our Alamofire response serializers, starting with .getGists:

²⁰https://github.com/settings/applications
Authentication 150

func getGists(urlRequest: URLRequestConvertible, completionHandler:


(Result<[Gist], NSError>, String?) -> Void) {
alamofireManager.request(urlRequest)
.validate()
.responseArray { (response:Response<[Gist], NSError>) in
if let urlResponse = response.response,
authError = self.checkUnauthorized(urlResponse) {
completionHandler(.Failure(authError), nil)
return
}

...
}
}

We get the NSHTTPURLResponse from the alamofire Response using urlResponse = response.response.
Then we call our new checkUnauthorized function and return the error if we get one. By adding
this call before the rest of the response serializer we can return immediately if we find an error.
And we need to handle that error when we load the gists:

func loadGists(urlToLoad: String?) {


self.isLoading = true
GitHubAPIManager.sharedInstance.getPublicGists(urlToLoad) { (result, nextPage) in
self.isLoading = false
self.nextPageURLString = nextPage

// tell refresh control it can stop showing up now


if self.refreshControl != nil && self.refreshControl!.refreshing {
self.refreshControl?.endRefreshing()
}

guard result.error == nil else {


print(result.error)
self.nextPageURLString = nil

self.isLoading = false
if let error = result.error {
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.showOAuthLoginView()
}
}
return
}
Authentication 151

if let fetchedGists = result.value {


if urlToLoad != nil {
self.gists += fetchedGists
} else {
self.gists = fetchedGists
}
}

// 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)

self.tableView.reloadData()
}
}

Test that out. Open the app and wait for the gists to load. Then revoke the app’s access²¹ and pull to
refresh the gists. You should get sent back to GitHub in Safari to re-authorize the app.

10.8 And That’s All


We worked through adding authentication to our API calls using basic auth, header-based auth or
OAuth 2.0. As we add more API calls we’ll be able to reuse that code and just worry about handling
the errors in the code that calls it.
If you got tired of typing, here’s the code: (tagged “auth”)²².
Now that we have authenticated calls we can add more API calls. Next we’ll let the users switch
between three lists of gists in the main table view.
²¹https://github.com/settings/applications
²²https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/auth
11. Switching Lists
Our requirements included being able to switch between 3 different lists of gists: public gists, starred
gists, and my gists. Let’s see how we can set up our single table view to let us do that.

Analyze your requirements to see what user interface components you need. Read through
this chapter as an example of handling multiple calls that return similar results. Similar
code could be used to display filters or search results if your API supports them.

11.1 Setting Up the UI


We already have the functions to make API calls for all 3 sets of gists. We just need to hook them up
to the user interface. It’d be handy to be able to switch between these sets of gists in our UI. Let’s
toss in a UISegmentedControl to do that. In the main storyboard drag a segmented control into the
navigation bar for the MasterViewController:

152
Switching Lists 153

Change the number of segments to 3:

Change the titles to “Public”, “Starred”, and “My Gists” (in that order):
Switching Lists 154

We’ll need to refer to the segmented control in code and get notified when the selected segment
changed. So in the MasterViewController add an IBOutlet:

class MasterViewController: UITableViewController {

...

@IBOutlet weak var gistSegmentedControl: UISegmentedControl!

...
}

And an IBAction:

@IBAction func segmentedControlValueChanged(sender: UISegmentedControl) {

And hook them up in the storyboard:


Switching Lists 155
Switching Lists 156

11.2 Sharing a Completion Handler


Now we need to load the correct data. So we’ll change our loadGists() function to check what the
segmented control is currently telling us to show. To make it easier to reuse the completion handler
for all 3 different functions we’ll declare it as a variable:

func loadGists(urlToLoad: String?) {


self.isLoading = false
let completionHandler: (Result<[Gist], NSError>, String?) -> Void =
{ (result, nextPage) in
self.isLoading = false
self.nextPageURLString = nextPage

// tell refresh control it can stop showing up now


if self.refreshControl != nil && self.refreshControl!.refreshing {
self.refreshControl?.endRefreshing()
}

guard result.error == nil else {


print(result.error)
self.nextPageURLString = nil

self.isLoading = false
if let error = result.error {
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.showOAuthLoginView()
}
}
return
}

if let fetchedGists = result.value {


if urlToLoad != nil {
self.gists += fetchedGists
} else {
self.gists = fetchedGists
}
}

// 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)
Switching Lists 157

self.tableView.reloadData()
}

switch gistSegmentedControl.selectedSegmentIndex {
case 0:
GitHubAPIManager.sharedInstance.getPublicGists(urlToLoad, completionHandler:
completionHandler)
case 1:
GitHubAPIManager.sharedInstance.getMyStarredGists(urlToLoad, completionHandler:
completionHandler)
case 2:
GitHubAPIManager.sharedInstance.getMyGists(urlToLoad, completionHandler:
completionHandler)
default:
print("got an index that I didn't expect for selectedSegmentIndex")
}
}

Since we’re using Alamofire’s Result struct we’ll have to import it in to this file:

import UIKit
import PINRemoteImage
import SafariServices
import Alamofire

class MasterViewController: UITableViewController, LoginViewDelegate, SFSafariViewControll\


erDelegate {
...
}

To declare a block as a variable or constant, we need to specify the type so the compiler knows where
it can be used. That’s what we’re doing here:

let completionHandler: (Result<[Gist], NSError>, String?) -> Void = { ... }

We’re declaring a block called completionHandler. It’s a constant and it takes in 2 arguments: a
Result that can hold either an array of gists or an error, and the URL for the next page of results
as a String. And it doesn’t return anything (Void). You’ll notice that it matches the declarations for
the completion handler arguments for getPublicGists, getMyStarredGists and getMyGists:
Switching Lists 158

func getPublicGists(pageToLoad: String?, completionHandler:


(Result<[Gist], NSError>, String?) -> Void) {
if let urlString = pageToLoad {
getGists(GistRouter.GetAtPath(urlString), completionHandler: completionHandler)
} else {
getGists(GistRouter.GetPublic(), completionHandler: completionHandler)
}
}

func getMyStarredGists(pageToLoad: String?, completionHandler:


(Result<[Gist], NSError>, String?) -> Void) {
if let urlString = pageToLoad {
getGists(GistRouter.GetAtPath(urlString), completionHandler: completionHandler)
} else {
getGists(GistRouter.GetMyStarred(), completionHandler: completionHandler)
}
}

func getMyGists(pageToLoad: String?, completionHandler:


(Result<[Gist], NSError>, String?) -> Void) {
if let urlString = pageToLoad {
getGists(GistRouter.GetAtPath(urlString), completionHandler: completionHandler)
} else {
getGists(GistRouter.GetMine(), completionHandler: completionHandler)
}
}

And in the router we need to add the GetMine case. It’s similar to the GetMyStarred case that we
added earlier when working on authentication:

enum GistRouter: URLRequestConvertible {


static let baseURLString:String = "https://api.github.com"

case GetPublic() // GET https://api.github.com/gists/public


case GetMyStarred() // GET https://api.github.com/gists/starred
case GetMine() // GET https://api.github.com/gists
case GetAtPath(String) // GET at given path

var URLRequest: NSMutableURLRequest {


var method: Alamofire.Method {
switch self {
case .GetPublic:
return .GET
case .GetMyStarred:
return .GET
Switching Lists 159

case .GetMine:
return .GET
case .GetAtPath:
return .GET
}
}

let result: (path: String, parameters: [String: AnyObject]?) = {


switch self {
case .GetPublic:
return ("/gists/public", nil)
case .GetMyStarred:
return ("/gists/starred", nil)
case .GetMine:
return ("/gists", nil)
case .GetAtPath(let path):
let URL = NSURL(string: path)
let relativePath = URL!.relativePath!
return (relativePath, nil)
}
}()

let URL = NSURL(string: GistRouter.baseURLString)!


let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))

// Set OAuth token if we have one


if let token = GitHubAPIManager.sharedInstance.OAuthToken {
URLRequest.setValue("token \(token)", forHTTPHeaderField: "Authorization")
}

let encoding = Alamofire.ParameterEncoding.JSON


let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)

encodedRequest.HTTPMethod = method.rawValue

return encodedRequest
}
}

And we need to add that call to the API manager:


Switching Lists 160

func getMyGists(pageToLoad: String?, completionHandler:


(Result<[Gist], NSError>, String?) -> Void) {
if let urlString = pageToLoad {
getGists(GistRouter.GetAtPath(urlString), completionHandler: completionHandler)
} else {
getGists(GistRouter.GetMine(), completionHandler: completionHandler)
}
}

Finally, we need to call loadGists when the user changes the selected segment so that the list of
gists that the user sees gets updated:

@IBAction func segmentedControlValueChanged(sender: UISegmentedControl) {


loadGists(nil)
}

11.3 And That’s All


Since we put in so much work in to designing our API manager earlier, adding this functionality
didn’t take too much effort. Save and run to test it out before moving on to creating a detail view in
the next chapter.
If you got tired of typing, here’s the code: (tagged “lists”)¹.
¹https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/lists
12. Switching Between View
Controllers and More JSON Parsing
Now we have a Swift app that:

• Pulls gist data from the GitHub Gists API¹ using Alamofire²
• Uses custom response serializers to process the JSON into an array of Gist objects
• Parses some string fields in the web service JSON
• Displays the results in a table view, including loading images from URLs in the table view
cells
• Lets the user select from 3 different lists with 3 different API calls
• Loads more results as the user scrolls down in the table view
• Allows users to pull to refresh the data

In this chapter we’ll keep progressing on this app to add more of the features required in “real” apps.
We will add:

• Parsing JSON including arrays of objects (files in this case) and strings to dates (with
NSDateFormatter)
• Passing data from a table view to a detail view, using a storyboard and segue. Tapping on a
row in the table view will open a detail view displaying additional data about that gist
• Creating a new view controller completely in code to display the content of the gist’s files.
Tapping on a filename will show the file’s content in a web view

Two of those features involve changing which view controller is shown but in very different ways.
The transition to the detail view uses a segue and the transition to a file’s content works by having
a navigation controller push a new view controller onto its stack.

12.1 JSON Parsing: Arrays & Dates


When our project is launched, it makes a call to the API to get a list of gists in JSON. The data passes
through a response serializer which parses the JSON to create Gist objects. So far we’ve only been
parsing a few string properties from the JSON for each gist. Now we’ll add extracting an array of
files and some the dates that the gist was created and last updated.
We’ll need to add some properties to our Gist model class:
¹https://developer.github.com/v3/gists/
²https://github.com/Alamofire/Alamofire

161
Switching Between View Controllers and More JSON Parsing 162

class Gist: ResponseJSONObjectSerializable {


var id: String?
var description: String?
var ownerLogin: String?
var ownerAvatarURL: String?
var url: String?
var files:[File]?
var createdAt:NSDate?
var updatedAt:NSDate?
...
}

And we’ll need a class to represent each of the files (a gist can have multiple files in it). For each file
we’ll just pull out the filename and the URL of the raw content so we can display it in a web view.
Let’s create a new File.swift class and implement the File class in it:

import SwiftyJSON

class File: ResponseJSONObjectSerializable {


var filename: String?
var raw_url: String?
}

Like the Gist class, our File class needs to be created from a chunk of JSON so it’ll implement the
ResponseJSONObjectSerializable protocol:

class File: ResponseJSONObjectSerializable {


var filename: String?
var raw_url: String?

required init?(json: JSON) {


self.filename = json["filename"].string
self.raw_url = json["raw_url"].string
}
}

And we need to extend the initializer for Gists to handle those files (we’ll get to the dates next):
Switching Between View Controllers and More JSON Parsing 163

required init?(json: JSON) {


self.description = json["description"].string
self.id = json["id"].string
self.ownerLogin = json["owner"]["login"].string
self.ownerAvatarURL = json["owner"]["avatar_url"].string
self.url = json["url"].string

// files
self.files = [File]()
if let filesJSON = json["files"].dictionary {
for (_, fileJSON) in filesJSON {
if let newFile = File(json: fileJSON) {
self.files?.append(newFile)
}
}
}

// TODO: dates
}

For the files we create an array: self.files = [File]()


The files are a dictionary within the JSON that looks like this:

"files": {
"ring.erl": {
"size": 932,
"raw_url": "https://gist.githubusercontent.com/raw/365370/8c4d2d43d178df44f4c03a7f2ac0\
ff512853564e/ring.erl",
"type": "text/plain",
"language": "Erlang",
"truncated": false,
"content": "content of gist"
},
...
}

So we loop through that dictionary and extract the filename and raw_url for each file:
Switching Between View Controllers and More JSON Parsing 164

if let filesJSON = json["files"].dictionary {


for (_, fileJSON) in filesJSON {
if let newFile = File(json: fileJSON) {
self.files?.append(newFile)
}
}
}

Since filesJSON is a dictionary we can loop over the key and value using for (_, fileJSON) in
filesJSON. The _ is placeholder for the key that indicates that we’re not using those values. Then we
try to create a new file with the JSON for a single file and add it to our array of files if it succeeds:

if let newFile = File(json: fileJSON) {


self.files?.append(newFile)
}

And that’s all we need to do to parse the array of files in the JSON.

12.2 Parsing Dates in JSON


What about the dates? How do we parse those? Since we’re getting the dates as strings and we want
to store them as NSDate objects, we’ll need an NSDateFormatter. An NSDateFormatter can convert
strings to NSDates and back.
Our dates look like "2014-12-10T16:44:31.486000Z". To set up a date formatter for those strings
we’ll want to specify the string format and set a few parameters to tell the date formatter what kind
of date it’s processing:

class func dateFormatter() -> NSDateFormatter {


let aDateFormatter = NSDateFormatter()
aDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
aDateFormatter.timeZone = NSTimeZone(abbreviation: "UTC")
aDateFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
return aDateFormatter
}

The locale and timeZone are useful for using NSDateFormatters with dates from servers, which you
often can’t guarantee to have the same language, time zone and cultural display of dates as your
users. Don’t use this approach when setting up dates to display to users. In that case you should use
date and time styles like NSDateFormatterShortStyle so that the user sees their dates and times in
the way that makes the most sense to them.
Now we need to apply that date formatter to the created and updated date strings:
Switching Between View Controllers and More JSON Parsing 165

class Gist {
...

required init?(json: JSON) {


...

// Dates
let dateFormatter = Gist.dateFormatter()
if let dateString = json["created_at"].string {
self.createdAt = dateFormatter.dateFromString(dateString)
}
if let dateString = json["updated_at"].string {
self.updatedAt = dateFormatter.dateFromString(dateString)
}
}

...
}

Creating NSDateFormatters or changing their properties can be very expensive computationally.


This code creates a new NSDateFormatter for each gist being parsed. If I found that the app was slow
that’s one of the first things I would optimize: having all of the gists share a single date formatter.
Here’s how to do it:

static let sharedDateFormatter = Gist.dateFormatter()

required init?(json: JSON) {


...

let dateFormatter = Gist.sharedDateFormatter


if let dateString = json["created_at"].string {
self.createdAt = dateFormatter.dateFromString(dateString)
}
if let dateString = json["updated_at"].string {
self.updatedAt = dateFormatter.dateFromString(dateString)
}
}

If we run the app now it doesn’t even look any different. Don’t fret, getting the data to display isn’t
all that difficult. Let’s do it by fixing up the detail view controller that Xcode created way back when
we created this project. We’ll rig it so that when we tap on the table view row for a gist we’ll get a
new view showing a bunch of its details.

Look back at the JSON for your model object class. Select a few more properties to display
in a detail view, maybe some dates or arrays. Add them to the class and the JSON parsing.
Switching Between View Controllers and More JSON Parsing 166

12.3 Configuring the Detail View Controller


Open up the DetailViewController file that Xcode generated. It looks like this:

import UIKit

class DetailViewController: UIViewController {

@IBOutlet weak var detailDescriptionLabel: UILabel!

var detailItem: AnyObject? {


didSet {
// Update the view.
self.configureView()
}
}

func configureView() {
// Update the user interface for the detail item.
if let detail = self.detailItem {
if let label = self.detailDescriptionLabel {
label.text = detail.description
}
}
}

override func viewDidLoad() {


super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.configureView()
}

override func didReceiveMemoryWarning() {


super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

Checking out the storyboard we can see that it’s a pretty simple view: it just shows a label that will
display the details for our detailObject.
Switching Between View Controllers and More JSON Parsing 167

Initial Detail View

We’ll want to improve that UI but first let’s change the detailObject from being a generic optional
AnyObject to being a Gist. Change the variable’s name in Detail View Controller from detailItem
to gist and declare it as a Gist:

var gist: Gist? {


didSet {
// Update the view.
self.configureView()
}
}

func configureView() {
// Update the user interface for the detail item.
if let currentGist = self.gist {
if let label = self.detailDescriptionLabel {
label.text = currentGist.description
}
}
}

Now where is this view getting the gist from? We’ll have to figure that out and change the variable
name there too. There are 2 easy ways to find it:
Switching Between View Controllers and More JSON Parsing 168

1. Search the project for the text “detailItem”


2. Try to build the project, it’ll fail wherever “detailItem” is used since that variable doesn’t exist
anymore

We shouldn’t be needing to hunt down all the uses of detailItem. If we were writing Objective-
C we could just right-click on it and select Refactor -> Rename. Sadly that functionality isn’t
implemented for Swift yet. Hopefully it’s coming soon.
.

So using either of those methods we’ll find 2 places outside of the Detail View Controller where
detailItem is used. So change both of them to refer to gist instead:
In the AppDelegate:

func splitViewController(splitViewController: UISplitViewController,


collapseSecondaryViewController secondaryViewController:UIViewController,
ontoPrimaryViewController primaryViewController:UIViewController) -> Bool {
guard let secondaryAsNavController = secondaryViewController as?
UINavigationController else { return false }
guard let topAsDetailController = secondaryAsNavController.topViewController
as? DetailViewController else { return false }
if topAsDetailController.gist == nil {
// Return true to indicate that we have handled the collapse by doing nothing
// the secondary controller will be discarded.
return true
}
return false
}

And in the MasterViewController where the gist from the table view row gets passed to the
DetailViewController for display. Here we need to changed the type from NSDate to Gist as
well as fixing the name. We’ll also change the code to check that the top view controller in the
navigation controller is a DetailViewController instead of assuming it, just in case we ever change
the navigation:
Switching Between View Controllers and More JSON Parsing 169

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {


if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let gist = gists[indexPath.row] as Gist
if let detailViewController = (segue.destinationViewController as!
UINavigationController).topViewController as?
DetailViewController {
detailViewController.gist = gist
detailViewController.navigationItem.leftBarButtonItem =
self.splitViewController?.displayModeButtonItem()
detailViewController.navigationItem.leftItemsSupplementBackButton = true
}
}
}
}

Change the detail item in your DetailViewController to your model object class. Fix any
other references to it in your project.

Now you can build & run. Tap on a gist and you should see a pretty boring display of its description:
Switching Between View Controllers and More JSON Parsing 170

Gist Description in Detail View


Switching Between View Controllers and More JSON Parsing 171

12.4 Passing Data in a Segue


But how does it work? Well, there’s a connection in the storyboard between the MasterViewCon-
troller table view cells and the DetailViewController (well, actually to the navigation controller
holding the DetailViewController but that’s just so we have a nice title bar):

Detail Segue

To zoom out when viewing the storyboard select Editor -> Canvas -> Zoom. Nice of them to hide
that so well, isn’t it?
.

So when a table view cell is tapped that segue is activated. The prepareForSegue function is called
when the transition (aka, “segue”) from the table view to the detail view starts.
In prepareForSegue this is what happens:

• Get the view controller we’re going to: let controller = (segue.destinationViewController
as! UINavigationController).topViewController as! DetailViewController
• Figure out which row they tapped using indexPathForSelectedRow(). That’s the currently
selected row. The indexPath gives us the row and section for it, in case we had a table view
with multiple sections
• Get the gist that corresponds to that row: let object = gists[indexPath.row]
• And pass that gist to the destination view controller: controller.gist = object
Switching Between View Controllers and More JSON Parsing 172

That’s what Xcode set up for us when we generated the Master-Detail project. We updated it to use
our Gist class earlier when we created that class:

// MARK: - Segues

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {


if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let gist = gists[indexPath.row] as Gist
if let detailViewController = (segue.destinationViewController as!
UINavigationController).topViewController as?
DetailViewController {
detailViewController.detailItem = gist
detailViewController.navigationItem.leftBarButtonItem =
self.splitViewController?.displayModeButtonItem()
detailViewController.navigationItem.leftItemsSupplementBackButton = true
}
}
}
}

12.5 Adding a Table View


Let’s improve the UI by displaying the bits of data we have in a table view. First we’ll set up our
DetailViewController as a UITableViewDataSource and UITableViewDelegate. We can do that
even though DetailViewController isn’t a UITableViewController. This approach is useful when
you want to use a table view as only part of a view, for example if you want to have a header above
the table view that doesn’t scroll with the table view. We’ll also need a reference to the table view
and we can get rid of that reference to the label:

class DetailViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {


@IBOutlet weak var tableView: UITableView!
...

Now we need to switch over to the main storyboard so we can add the table view. Open the
Main.Storyboard and select the DetailViewController:
Switching Between View Controllers and More JSON Parsing 173

Select Detail View Controller

In the Detail View Controller, delete the label and drag in a table view. Drag it around until it fills
the view:

Add Table View


Switching Between View Controllers and More JSON Parsing 174

Align Table View

Add a single prototype cell to the table view. Set its identifier to “Cell”.

Add Prototype Cell

Set Cell ID

In the top of the right panel, choose the connection organizer (last tab, the icon is an arrow in a
circle). Hook up the Detail View Controller as the table view’s data source and delegate. To do so,
select the table view then drag connections from those circles to the Detail View Controller (yellow
icon above the view). And hook up the table view IBOutlet from the Detail View Controller to the
table view:
Switching Between View Controllers and More JSON Parsing 175

Connect Table View IBOutlet

Connect Data Source and Delegate

Now we need to go back to the Detail View Controller and fill in the data for that table view to
display. We’ll have 2 sections: the general data about the gist like its description and a list of the files
in the gist. Then if they tap on a file we’ll display its content:
Switching Between View Controllers and More JSON Parsing 176

So we have 2 sections and we can give them titles. The first section has 2 items in it (for the
description and owner) and the second one has enough rows to show all of the files:

func numberOfSectionsInTableView(tableView: UITableView) -> Int {


return 2
}

func tableView(tableView: UITableView,


numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 2
} else {
return gist?.files?.count ?? 0
}
}

func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {


if section == 0 {
return "About"
} else {
return "Files"
}
}

?? is the nil coalescing operator. It just means “give me this value, unless it’s nil, then give me this
default value”. So return gist?.files?.count ?? 0 will return the number of files, unless we don’t
have any files, then we’ll get 0 instead of nil.
We can use the data in the gist to populate those cells:

func tableView(tableView: UITableView, cellForRowAtIndexPath


indexPath: NSIndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)

if indexPath.section == 0 {
if indexPath.row == 0 {
cell.textLabel?.text = gist?.description
} else if indexPath.row == 1 {
cell.textLabel?.text = gist?.ownerLogin
}
} else {
if let file = gist?.files?[indexPath.row] {
cell.textLabel?.text = file.filename
}
Switching Between View Controllers and More JSON Parsing 177

}
return cell
}

And we’ll need to tell the table view to reload after the gist is set:

func configureView() {
// Update the user interface for the detail item.
if let detailsView = self.tableView {
detailsView.reloadData()
}
}

Build & run to make sure that works.

Set up a table view or custom view using IBOutlets in DetailViewController to show the
details for the objects in your list.

Gist Details Displayed in Table View


Switching Between View Controllers and More JSON Parsing 178

While it’s nice to see the list of files, this view really isn’t all that useful. Let’s add 2 things to make
it better:

1. Showing the file content when they tap on a filename


2. Let users star & unstar the gist

12.6 Displaying Gist File Content


When parsing the JSON didn’t actually get the content for the files, we just parsed out a URL where
the raw content of the file exist. We can display a URL using a web view. There are certainly nicer
ways to do it but a web browser window will work.
We added the Safari Services framework back when we set up (OAuth)[#oauth]. If we hadn’t done
that we’d need to add it to our project now.
Import the SafariServices framework in DetailViewController:

import UIKit
import SafariServices

class DetailViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {


...
}

Then hook it up when they select a row in that second section:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {


if indexPath.section == 1 {
if let file = gist?.files?[indexPath.row],
urlString = file.raw_url,
url = NSURL(string: urlString) {
let safariViewController = SFSafariViewController(URL: url)
safariViewController.title = file.filename
self.navigationController?.pushViewController(safariViewController, animated: true)
}
}
}

So when they tap a filename, we get the url:


Switching Between View Controllers and More JSON Parsing 179

if let file = gist?.files?[indexPath.row],


urlString = file.raw_url,
url = NSURL(string: urlString) {

Then we create a Safari view controller with our URL, showing the filename in the navigation bar
at the top of the screen:

let safariViewController = SFSafariViewController(URL: url)


safariViewController.title = file.filename

And use our navigation controller to switch to that view:

self.navigationController?.pushViewController(safariViewController, animated: true)

Using the navigation controller means that they’ll get a back button or be able to use a swipe gesture
to go back. We don’t need to add any buttons to our new view controller for that to work.

Consider if any of your content would be best shown in a web view. If so, implement it by
following the steps in this section.

12.7 And That’s All


Build and run to test that out. Next chapter we’ll add additional web service calls to star & unstar
the gist.
If you got tired of typing, here’s the code: (tagged “detail”)³.
³https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/detail
13. Adding More API Calls - Starring
This chapter is all about hooking in to additional web services to display details. Our DetailView-
Controller will call three more API endpoints:

• Loading more data from another web service: Is a gist starred?


• PUT & DELETE: Star & unstar gists

One bit of data that we didn’t get in the JSON about a gist is whether or not we’ve starred it. There’s
a whole other API call for that: GET /gists/:id/star. According to the documentation we’ll get a
204 No Content response if we have starred the gist and a 404 Not Found response if we haven’t.

13.1 Is the Gist Starred?


To set up starring we first need to know whether we’ve already starred a gist. So we’ll pop over
to our GitHubAPIManager class and add a function for that. We can use .validate(statusCode:
204...204) to check for the 204 response so the rest of this function is pretty simple:

// MARK: Starring / Unstarring / Star status


func isGistStarred(gistId: String, completionHandler: Result<Bool, NSError> -> Void) {
// GET /gists/:id/star
alamofireManager.request(GistRouter.IsStarred(gistId))
.validate(statusCode: [204])
.response { (request, response, data, error) in
// 204 if starred, 404 if not
if let error = error {
print(error)
if response?.statusCode == 404 {
completionHandler(.Success(false))
return
}
completionHandler(.Failure(error))
return
}
completionHandler(.Success(true))
}
}

And in the router:

180
Adding More API Calls - Starring 181

enum GistRouter: URLRequestConvertible {


static let baseURLString:String = "https://api.github.com"

...
case IsStarred(String) // GET https://api.github.com/gists/\(gistId)/star

var URLRequest: NSMutableURLRequest {


var method: Alamofire.Method {
switch self {
...
case .IsStarred:
return .GET
}
}

let result: (path: String, parameters: [String: AnyObject]?) = {


switch self {
...
case .IsStarred(let id):
return ("/gists/\(id)/star", nil)
}
}()

...

return encodedRequest
}
}

13.2 Starred Status in the Table View


In our DetailViewController we’ll want a variable to say if the gist has been starred or if we haven’t
gotten a response from that API call yet. This is the perfect place for an optional:

var isStarred: Bool?

If isStarred is nil then we haven’t gotten a response yet. Otherwise the true/false value indicates
whether it’s starred or not. So let’s call the isGistStarred function when this view is shown:
Adding More API Calls - Starring 182

func configureView() {
// Update the user interface for the detail item.
if let _: Gist = self.gist {
fetchStarredStatus()
if let detailsView = self.tableView {
detailsView.reloadData()
}
}
}

func fetchStarredStatus() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.isGistStarred(gistId, completionHandler: {
result in
if let error = result.error {
print(error)
}
if let status = result.value where self.isStarred == nil { // just got it
self.isStarred = status
// TODO: update display
}
})
}
}

To display the starred status we can add a third row to the first section of the table view, based on
whether isStarred is nil or not. To animate adding that row when we get the API response we can
use tableView?.insertRowsAtIndexPaths with a single indexPath (3rd row in the 1st section):

func fetchStarredStatus() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.isGistStarred(gistId, completionHandler: {
result in
if let error = result.error {
print(error)
}
if let status = result.value where self.isStarred == nil { // just got it
self.isStarred = status
self.tableView?.insertRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}
Adding More API Calls - Starring 183

And we need to update our table view data source methods to know what to display in that row:

func tableView(tableView: UITableView,


numberOfRowsInSection section: Int) -> Int {
if section == 0 {
if let _ = isStarred {
return 3
}
return 2
} else {
return gist?.files?.count ?? 0
}
}

func tableView(tableView: UITableView, cellForRowAtIndexPath


indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)

if indexPath.section == 0 {
if indexPath.row == 0 {
cell.textLabel?.text = gist?.description
} else if indexPath.row == 1 {
cell.textLabel?.text = gist?.ownerLogin
} else {
if let starred = isStarred {
if starred {
cell.textLabel?.text = "Unstar"
} else {
cell.textLabel?.text = "Star"
}
}
}
} else {
if let file = gist?.files?[indexPath.row] {
cell.textLabel?.text = file.filename
// TODO: add disclosure indicators
}
}
return cell
}

In the table view cell we’ll display “Star” if it’s not starred and “Unstar” if it is starred. So we’ll need
to take those actions if they tap on that row:
Adding More API Calls - Starring 184

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {


if indexPath.section == 0 {
if indexPath.row == 2 { // star or unstar
if let starred = isStarred {
if starred {
// unstar
unstarThisGist()
} else {
// star
starThisGist()
}
}
}
} else if indexPath.section == 1 {
...
}
}

13.3 PUT and DELETE Calls to Star and Unstar Gists


To implement starThisGist() and unstarThisGist() we need two more simple API calls in
GitHubAPIManager (getting used to adding those yet?):

func starGist(gistId: String, completionHandler: (NSError?) -> Void) {


alamofireManager.request(GistRouter.Star(gistId))
.response { (request, response, data, error) in
if let error = error {
print(error)
return
}
completionHandler(error)
}
}

func unstarGist(gistId: String, completionHandler: (NSError?) -> Void) {


alamofireManager.request(GistRouter.Unstar(gistId))
.response { (request, response, data, error) in
if let error = error {
print(error)
return
}
completionHandler(error)
}
}
Adding More API Calls - Starring 185

And in the router:

enum GistRouter: URLRequestConvertible {


static let baseURLString:String = "https://api.github.com"

...
case Star(String) // PUT https://api.github.com/gists/\(gistId)/star
case Unstar(String) // DELETE https://api.github.com/gists/\(gistId)/star

var URLRequest: NSMutableURLRequest {


var method: Alamofire.Method {
switch self {
...
case .Star:
return .PUT
case .Unstar:
return .DELETE
}
}

let result: (path: String, parameters: [String: AnyObject]?) = {


switch self {
...
case .Star(let id):
return ("/gists/\(id)/star", nil)
case .Unstar(let id):
return ("/gists/\(id)/star", nil)
}
}()

...

return encodedRequest
}
}

In DetailViewController we can call those functions using our gist’s ID, update the isStarred
status, and reload the row in the table view to reflect that status:
Adding More API Calls - Starring 186

func starThisGist() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.starGist(gistId, completionHandler: {
(error) in
if let error = error {
print(error)
} else {
self.isStarred = true
self.tableView.reloadRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}

func unstarThisGist() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.unstarGist(gistId, completionHandler: {
(error) in
if let error = error {
print(error)
} else {
self.isStarred = false
self.tableView.reloadRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}

Go back to your requirements. Choose some API calls that haven’t been used in your app
yet. Add functions to make those API calls to your API manager and router. Integrate them
into your app’s UI. Don’t pick calls to create or delete objects yet, we’ll handle those in the
next few chapters.

13.4 Authorization Check


We should add a check that we’re still authorized to these calls, like we did with getGists. That
way the user will see a more useful error message in those cases:
Adding More API Calls - Starring 187

func isGistStarred(gistId: String, completionHandler: Result<Bool, NSError> -> Void) {


alamofireManager.request(GistRouter.IsStarred(gistId))
.validate(statusCode: [204])
.response { (request, response, data, error) in
if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) {
completionHandler(.Failure(authError))
return
}
...
}
}

func starGist(gistId: String, completionHandler: (NSError?) -> Void) {


alamofireManager.request(GistRouter.Star(gistId))
.response { (request, response, data, error) in
if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) {
completionHandler(authError)
return
}
...
}
}

func unstarGist(gistId: String, completionHandler: (NSError?) -> Void) {


alamofireManager.request(GistRouter.Unstar(gistId))
.response { (request, response, data, error) in
if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) {
completionHandler(authError)
return
}
...
}
}

The existing checkUnauthorized function will take care of creating an appropriate error in those
cases. We’ll need to update the methods in DetailViewController so the user actually sees it:
Adding More API Calls - Starring 188

class DetailViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {


...
var alertController: UIAlertController?
...

func fetchStarredStatus() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.isGistStarred(gistId, completionHandler: {
result in
if let error = result.error {
print(error)
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title:
"Could not get starred status", message: error.description,
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true,
completion: nil)
}
}

if let status = result.value where self.isStarred == nil { // just got it


self.isStarred = status
self.tableView?.insertRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}

...
}

We can check the error domain and code: error.domain == NSURLErrorDomain && error.code
== NSURLErrorUserAuthenticationRequired. If they match our OAuth error then we can show a
UIAlertController with our error’s description.

Starring and unstarring are similar, except for those we’ll show a generic error if the call fails. Since
displaying the starred status isn’t something the user specifically requested we won’t interrupt their
use of the app to show an error dialog if it didn’t work:
Adding More API Calls - Starring 189

func starThisGist() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.starGist(gistId, completionHandler: {
(error) in
if let error = error {
print(error)
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title: "Could not star gist",
message: error.description, preferredStyle: .Alert)
} else {
self.alertController = UIAlertController(title: "Could not star gist",
message: "Sorry, your gist couldn't be starred. " +
"Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
}
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true, completion: nil)
} else {
self.isStarred = true
self.tableView.reloadRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}

func unstarThisGist() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.unstarGist(gistId, completionHandler: {
(error) in
if let error = error {
print(error)
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title: "Could not unstar gist",
message: error.description, preferredStyle: .Alert)
} else {
self.alertController = UIAlertController(title: "Could not unstar gist",
message: "Sorry, your gist couldn't be unstarred. " +
" Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
Adding More API Calls - Starring 190

}
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true, completion: nil)
} else {
self.isStarred = false
self.tableView.reloadRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}

If necessary, add auth checks to your API calls.

13.5 And That’s All


And finally that’s it for this detail view! Run it and check out all your deets. Enjoy the sweet
animation when the isStarred status gets loaded just after the view appears.
If you got tired of typing, here’s the code: (tagged “star”)¹.
So what next? We only have a few chapters left but we’ll still be adding two key features: deleting
and creating gists. After that we’ll figure out what happens to our very web-dependent app if the
user doesn’t have an internet connection. Then we’ll wrap up with some ideas about how you could
extend this app or make your own API-driven Swift app.
¹https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/star
14. Deleting Gists
Deleting gists isn’t much more difficult than starring and unstarring gists. We just need to check
the documentation to find out what API calls need to be made, implement calling them in our
GitHubAPIManager, and then hook that up to our user interface.

Like starring, deleting gists requires authentication. You can’t just go around deleting other users’
gists.

14.1 DELETE API Call


To delete a gist we need to make an authenticated call to:

https://api.github.com/gists/*gistID*

We can implement this call in our GitHubAPIManager, including checking that their OAuth
credentials are still valid:

func deleteGist(gistId: String, completionHandler: (NSError?) -> Void) {


alamofireManager.request(GistRouter.Delete(gistId))
.response { (request, response, data, error) in
if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) {
completionHandler(authError)
return
}
if let error = error {
print(error)
return
}
completionHandler(error)
}
}

And in the router:

191
Deleting Gists 192

enum GistRouter: URLRequestConvertible {


static let baseURLString:String = "https://api.github.com"

...
case Delete(String) // DELETE https://api.github.com/gists/\(gistId)

var URLRequest: NSMutableURLRequest {


var method: Alamofire.Method {
switch self {
...
case .Delete:
return .DELETE
}
}

let result: (path: String, parameters: [String: AnyObject]?) = {


switch self {
...
case .Delete(let id):
return ("/gists/\(id)", nil)
}
}()

...

return encodedRequest
}
}

Be careful! Deleting gists really deletes them so don’t delete gists you want to keep. Now
would be a good time to go create a bunch of private test gists that you can delete to test
this feature.

If you can delete objects in your table view then set up the API call to do so in your API
manager now.

14.2 User Interface: Table View Delete Features


Table views have some great UI already built in for handling deletion. Swipe to delete is now
commonly understood by users so there’s no need to implement your own different way to handle
it.
Deleting Gists 193

Setting up swipe to delete for our table view in MasterViewController only takes a few steps. First
we need to let the table view know that the rows can be deleted. But we only want to do that if
we’re looking at our gists, not if we’re looking at the lists of public or starred gists:

override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath:


NSIndexPath) -> Bool {
// only allow editing my gists
return gistSegmentedControl.selectedSegmentIndex == 2
}

We can also add an edit button so that they can switch to edit mode to easily delete multiple gists.
We’ll add it when they switch to viewing their gists and remove it for public and starred gists:

@IBAction func segmentedControlValueChanged(sender: UISegmentedControl) {


// only show add button for my gists
if (gistSegmentedControl.selectedSegmentIndex == 2) {
self.navigationItem.leftBarButtonItem = self.editButtonItem()
} else {
self.navigationItem.leftBarButtonItem = nil
}
loadGists(nil)
}

And remove it from viewDidLoad so it doesn’t show up when we first load the public gists:

override func viewDidLoad() {


super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.navigationItem.leftBarButtonItem = self.editButtonItem()

let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self,


action: "insertNewObject:")
self.navigationItem.rightBarButtonItem = addButton
if let split = self.splitViewController {
let controllers = split.viewControllers
self.detailViewController = (controllers[controllers.count-1] as!
UINavigationController).topViewController as? DetailViewController
}
}

Then we need to tell the table view what to do when the user swipes and taps the delete button.
We’ll take 3 actions:
Deleting Gists 194

1. Delete the gist from our array of gists


2. Remove the row from the table view. We can use tableView.deleteRowsAtIndexPaths instead
of reloading all of the rows
3. Make the API call to delete the gist

Here’s how to do that:

override func tableView(tableView: UITableView, commitEditingStyle editingStyle:


UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
gists.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
// delete from API
if let id = gists[indexPath.row].id {
GitHubAPIManager.sharedInstance.deleteGist(id, completionHandler: {
(error) in
print(error)
})
}
}
}

Now it might seem a bit odd that we’re removing the gist from the table view before it’s actually been
deleted. It makes for a good user experience though: they’ll see the reaction to their tap immediately
instead of having to wait for the server to respond to the API call.
But what if there’s a problem and the API call fails? Then we’ll just have to put the gist back. That’s
a rare enough occurrence that we’ll handle it by putting the gist back in our array and table view.
We should also tell the user that their attempt to delete the gist didn’t work:

override func tableView(tableView: UITableView, commitEditingStyle editingStyle:


UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
let gistToDelete = gists.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
// delete from API
if let id = gists[indexPath.row].id {
GitHubAPIManager.sharedInstance.deleteGist(id, completionHandler: {
(error) in
print(error)
if let _ = error {
// Put it back
self.gists.insert(gistToDelete, atIndex: indexPath.row)
tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Right)
Deleting Gists 195

// tell them it didn't work


let alertController = UIAlertController(title: "Could not delete gist",
message: "Sorry, your gist couldn't be deleted. Maybe GitHub is "
+ "down or you don't have an internet connection.",
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(okAction)
// show the alert
self.presentViewController(alertController, animated:true, completion: nil)
}
})
}
}
}

Integrate your delete call with the table view’s built-in delete features.

14.3 And That’s All


That’s all it takes to add deleting gists as a feature in our app. Adding swipe to delete and an edit
mode are easy when you use a table view:
If you got tired of typing, here’s the code: (tagged “delete”)¹.
¹https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/delete
15. Creating Gists and Clearing the
Cache
Creating gists is a bit more work because we need to get some input from the user. So we’ll create a
new view to collect that input. But first let’s look at the API call we need to make.
Creating gists can be done anonymously but we’ll stick to creating gists that are owned by the
current user since we already have the authentication built into our app.

15.1 POST API Call with Nested JSON Parameters


To create a gist we POST to https://api.github.com/gists. We need to pass in the data to generate
the gist as JSON that looks like this:

{
"description": "the description for this gist",
"public": true,
"files": {
"file1.txt": {
"content": "String file content"
}
}
}

So we need two bits of input about the gist:

1. Description: String
2. Public or private: Boolean

And two bits of info about each file in it:

1. File name: String


2. File content: String

So let’s figure out how to take those inputs and turn them into the JSON that the API call expects.
We already have a class that represents File objects so we’ll use that:

196
Creating Gists and Clearing the Cache 197

func createNewGist(description: String, isPublic: Bool, files: [File],


completionHandler: (Result<Bool, NSError>) -> Void) {

Ok, so we’ve got the data passed in, now how to turn it into JSON? Alamofire expects the JSON
parameters as a [String: AnyObject] dictionary. It can be made up of arrays, dictionaries and
strings.
First let’s convert the isPublic boolean value to a string:

let publicString: String


if isPublic {
publicString = "true"
} else {
publicString = "false"
}

Then we’ll tackle the Files. We’ll create a little JSON dictionary of files that’ll look like this:

"file1.txt": {
"content": "String file1 content"
}
"file2.txt": {
"content": "String file2 content"
}
...

Our File object needs a content property and we’ll need to be able to create File objects with
names and content:

class File: ResponseJSONObjectSerializable {


var filename: String?
var raw_url: String?
var content: String?

required init?(json: JSON) {


self.filename = json["filename"].string
self.raw_url = json["raw_url"].string
}

init?(aName: String?, aContent: String?) {


self.filename = aName
self.content = aContent
}
}

Back in CreateNewGist let’s create that dictionary and then add an entry for each File object:
Creating Gists and Clearing the Cache 198

var filesDictionary = [String: AnyObject]()


for file in files {
if let name = file.filename, content = file.content {
filesDictionary[name] = ["content": content]
}
}

Then we can put all the bits together in one dictionary:

let parameters:[String: AnyObject] = [


"description": description,
"isPublic": publicString,
"files" : filesDictionary
]

We need to add the API call to the router:

enum GistRouter: URLRequestConvertible {


static let baseURLString:String = "https://api.github.com"

...
case Create([String: AnyObject]) // POST https://api.github.com/gists

var URLRequest: NSMutableURLRequest {


var method: Alamofire.Method {
switch self {
...
case .Create:
return .POST
}
}

let result: (path: String, parameters: [String: AnyObject]?) = {


switch self {
...
case .Create(let params):
return ("/gists", params)
}
}()

...

return encodedRequest
}
}
Creating Gists and Clearing the Cache 199

When creating the URL request the router will add the parameters and specify that they’re JSON.
We’ve had that code in the router for a while but we haven’t used it yet:

let encoding = Alamofire.ParameterEncoding.JSON


let (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters)

Now we’re set to make the request:

alamofireManager.request(GistRouter.Create(parameters))
.response { (request, response, data, error) in
if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) {
completionHandler(.Failure(authError))
return
}
if let error = error {
print(error)
completionHandler(.Success(false))
return
}
completionHandler(.Success(true))
}

So putting all of that together:

func createNewGist(description: String, isPublic: Bool, files: [File], completionHandler:


Result<Bool, NSError> -> Void) {
let publicString: String
if isPublic {
publicString = "true"
} else {
publicString = "false"
}

var filesDictionary = [String: AnyObject]()


for file in files {
if let name = file.filename, content = file.content {
filesDictionary[name] = ["content": content]
}
}
let parameters:[String: AnyObject] = [
"description": description,
"isPublic": publicString,
"files" : filesDictionary
Creating Gists and Clearing the Cache 200

alamofireManager.request(GistRouter.Create(parameters))
.response { (request, response, data, error) in
if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) {
completionHandler(.Failure(authError))
return
}
if let error = error {
print(error)
completionHandler(.Success(false))
return
}
completionHandler(.Success(true))
}
}

If you have one, add a POST call to create a new object to your API manager.

Now to create a UI to get the input from the user.

15.2 Creating an Input Form with Validation


We’ll create a form to get the info from the user using the XLForm¹ library. XLForm is a commonly
used library to create forms in iOS with built-in validation.
Add XLForm v3.0 to your project using CocoaPods.
We’ll only want the user to be able to access this form when they’re looking at their gists. Like the
edit button, we’ll add a + button to the top right corner of the view controller when they switch to
that list of gists and remove it when they switch to public or starred gists (in MasterViewController).
Unlike the edit button a + button doesn’t already have a default action hooked up to it so we’ll use
the insertNewObject: function to handle that:

¹https://github.com/xmartlabs/XLForm
Creating Gists and Clearing the Cache 201

@IBAction func segmentedControlValueChanged(sender: UISegmentedControl) {


// only show add button for my gists
if (gistSegmentedControl.selectedSegmentIndex == 2) {
self.navigationItem.leftBarButtonItem = self.editButtonItem()
let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self,
action: "insertNewObject:")
self.navigationItem.rightBarButtonItem = addButton
} else {
self.navigationItem.leftBarButtonItem = nil
self.navigationItem.rightBarButtonItem = nil
}
loadGists(nil)
}

And we need to remove setting the add button in viewDidLoad:

override func viewDidLoad() {


super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.

let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self,


action: "insertNewObject:")
self.navigationItem.rightBarButtonItem = addButton
if let split = self.splitViewController {
let controllers = split.viewControllers
self.detailViewController = (controllers[controllers.count-1] as!
UINavigationController).topViewController as? DetailViewController
}
}

Our insertNewObject: function will display the form (that we haven’t created yet):

// MARK: - Creation
func insertNewObject(sender: AnyObject) {
let createVC = CreateGistViewController(nibName: nil, bundle: nil)
self.navigationController?.pushViewController(createVC, animated: true)
}

Guess we’d better code up the CreateGistViewController form or that’s going to crash. Create a
new file called CreateGistViewController.swift. Import the XLForm library and make your class
a subclass of XLFormViewController:
Creating Gists and Clearing the Cache 202

import Foundation
import XLForm

class CreateGistViewController: XLFormViewController {


}

Now we need to add our fields to the form. To keep it simple we’ll only allow adding a single
file to a gist for now. Since we’re creating a custom UIViewController (which is a parent class to
XLFormViewController) we need to provide a custom initializer: required init(coder aDecoder:
NSCoder). All that it’ll do is call the parent init method and then our initializeForm function.

We’ll also want to initialize our form if they create a form from a xib or storyboard file so we’ll
override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!):

class CreateGistViewController: XLFormViewController {


required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.initializeForm()
}

override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {


super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
self.initializeForm()
}

private func initializeForm() {


...
}
}

Now we can add the fields to the form in initializeForm. XLFormViewController knows what
fields to display from a XLFormDescriptor object. So we’ll create one of those to describe our form
then add sections and rows to it. Unlike a table view we can just add all the sections and rows when
we create it. Each row has a few properties: its type and title as well as a tag. The tag is used to get
access to the rows later. We can also specify whether a row is required:
Creating Gists and Clearing the Cache 203

private func initializeForm() {


let form = XLFormDescriptor(title: "Gist")

// Section 1
let section1 = XLFormSectionDescriptor.formSection() as XLFormSectionDescriptor
form.addFormSection(section1)

let descriptionRow = XLFormRowDescriptor(tag: "description", rowType:


XLFormRowDescriptorTypeText, title: "Description")
descriptionRow.required = true
section1.addFormRow(descriptionRow)

let isPublicRow = XLFormRowDescriptor(tag: "isPublic", rowType:


XLFormRowDescriptorTypeBooleanSwitch, title: "Public?")
isPublicRow.required = false
section1.addFormRow(isPublicRow)

let section2 = XLFormSectionDescriptor.formSectionWithTitle("File 1") as


XLFormSectionDescriptor
form.addFormSection(section2)

let filenameRow = XLFormRowDescriptor(tag: "filename", rowType:


XLFormRowDescriptorTypeText, title: "Filename")
filenameRow.required = true
section2.addFormRow(filenameRow)

let fileContent = XLFormRowDescriptor(tag: "fileContent", rowType:


XLFormRowDescriptorTypeTextView, title: "File Content")
fileContent.required = true
section2.addFormRow(fileContent)

self.form = form
}

We aren’t setting the isPublicRow as required since the user doesn’t actually have to tap the switch.
They can just leave it as the default value and we’ll know how to handle that.
We’ll also want a few buttons on this form: a Cancel button in the top left corner and a Save button in
the top right corner. When they cancel we’ll just go back to the list of gists. We’ll have to implement
saving after we’ve got the form all set up:
Creating Gists and Clearing the Cache 204

class CreateGistViewController: XLFormViewController {


override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem:
UIBarButtonSystemItem.Cancel, target: self, action: "cancelPressed:")
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem:
UIBarButtonSystemItem.Save, target: self, action: "savePressed:")
}

func cancelPressed(button: UIBarButtonItem) {


self.navigationController?.popViewControllerAnimated(true)
}

func savePressed(button: UIBarButtonItem) {


// TODO: implement
}

...
}

Add a form to your app to create a new object. We’ll add the validation and hook it up to
the POST call in the next section.

If you save and run your app now you should be able to see the form come up when you tap on the +
button on your list of gists. You can fill out the fields but tapping the save button won’t do anything.
Let’s fix that. First we’ll use the built-in XLForm formValidationErrors() function to make sure
they filled out all of the required fields. If validation does find some errors we’ll display them (again
with a built-in function: showFormValidationError) and let them fix them:

func savePressed(button: UIBarButtonItem) {


let validationErrors = self.formValidationErrors() as? [NSError]
if validationErrors?.count > 0 {
self.showFormValidationError(validationErrors!.first)
return
}

If they don’t have any errors then we can turn off editing mode on the table view:

self.tableView.endEditing(true)

And then we can pull the data out of the form. To get a value from the form we use the tag that we
set earlier like this, with the relevant type (String or Bool for our form):
Creating Gists and Clearing the Cache 205

form.formRowWithTag("tagForRow")?.value as? Type

So let’s use that. First isPublic, which won’t have a value if they didn’t tap on the switch so we
need to set it to false if that’s the case:

let isPublic: Bool


if let isPublicValue = form.formRowWithTag("isPublic")?.value as? Bool {
isPublic = isPublicValue
} else {
isPublic = false
}

Then the String properties. We can use a single if-let statement to get the values for all three text
entry sections (which won’t be blank because the validation already checked for that). Then we can
create a File object with itas properties:

if let description = form.formRowWithTag("description")?.value as? String,


filename = form.formRowWithTag("filename")?.value as? String,
fileContent = form.formRowWithTag("fileContent")?.value as? String {
var files = [File]()
if let file = File(aName: filename, aContent: fileContent) {
files.append(file)
}

And finally we can make the API call using the user’s input. If it fails we’ll show them an error
(ironically, GitHub is down while I’m writing this section so it’s easy to test). If it succeeds then
we’ll return to the list of my gists:

GitHubAPIManager.sharedInstance.createNewGist(description, isPublic: isPublic,


files: files, completionHandler: {
result in
guard result.error == nil, let successValue = result.value
where successValue == true else {
if let error = result.error {
print(error)
}
let alertController = UIAlertController(title: "Could not create gist",
message: "Sorry, your gist couldn't be deleted. " +
"Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(okAction)
Creating Gists and Clearing the Cache 206

self.presentViewController(alertController, animated:true, completion: nil)


return
}
self.navigationController?.popViewControllerAnimated(true)
})
}
}

Putting that all together:

func savePressed(button: UIBarButtonItem) {


let validationErrors = self.formValidationErrors() as? [NSError]
if validationErrors?.count > 0 {
self.showFormValidationError(validationErrors!.first)
return
}
self.tableView.endEditing(true)
let isPublic: Bool
if let isPublicValue = form.formRowWithTag("isPublic")?.value as? Bool {
isPublic = isPublicValue
} else {
isPublic = false
}
if let description = form.formRowWithTag("description")?.value as? String,
filename = form.formRowWithTag("filename")?.value as? String,
fileContent = form.formRowWithTag("fileContent")?.value as? String {
var files = [File]()
if let file = File(aName: filename, aContent: fileContent) {
files.append(file)
}
GitHubAPIManager.sharedInstance.createNewGist(description, isPublic: isPublic,
files: files, completionHandler: {
result in
guard result.error == nil, let successValue = result.value
where successValue == true else {
if let error = result.error {
print(error)
}
let alertController = UIAlertController(title: "Could not create gist",
message: "Sorry, your gist couldn't be deleted. " +
"Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(okAction)
Creating Gists and Clearing the Cache 207

self.presentViewController(alertController, animated:true, completion: nil)


return
}
self.navigationController?.popViewControllerAnimated(true)
})
}
}

Now that will work for creating gists. Save & run to test it out.

Hook up your form with validation and the creation API call.

Save and run. Notice anything wrong? Our new gist isn’t getting added to the list of gists when we
go back to the MasterViewController even though loadGists runs when we go back to that screen:

override func viewDidAppear(animated: Bool) {


super.viewDidAppear(animated)

let defaults = NSUserDefaults.standardUserDefaults()


if (!defaults.boolForKey("loadingOAuthToken")) {
loadInitialData()
}
}

It looks like we’re getting a cached version of the response. We’ll need to bypass that by telling
Alamofire that we explicitly don’t want the cached response.
Under the hood Alamofire uses NSURLCache so we can create a simple method in GitHubAPIManager
to clear the cache:

func clearCache() {
let cache = NSURLCache.sharedURLCache()
cache.removeAllCachedResponses()
}

And then if we succeed in creating a new gist we can clear the cache so it gets loaded correctly:
Creating Gists and Clearing the Cache 208

func createNewGist(description: String, isPublic: Bool, files: [File],


completionHandler: (Bool?, NSError?) -> Void) {
...

alamofireManager.request(GistRouter.Create(parameters))
.response { (request, response, data, error) in
if let urlResponse = response, authError = self.checkUnauthorized(urlResponse) {
completionHandler(.Failure(authError))
return
}
if let error = error {
print(error)
completionHandler(.Success(false))
return
}
self.clearCache()
completionHandler(.Success(true))
}
}

15.3 And That’s All


Save and run to make sure that the new gists are displayed in the list now right after they’re created.
If you got tired of typing, here’s the code: (tagged “add”)².
²https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/add
16. What if They’re Offline?
One of the simplest ways to get your app rejected during App Store review is to not handle a lack of
internet connection. There are lots of ways that different apps handle being offline, it really depends
on your data and what your users are trying to do. At a high level, a few options are:

• Tell them they need an internet connection


• Go into a read-only mode with whatever data you already have cached
• Let them interact with the app as if they were online and sync up with the API when they
reconnect

If your app does end up without an internet connection it should keep checking if the connection
has been restored. Don’t assume that once the connection is gone that it won’t come back.
When writing a new app it’s often easiest to start with the simplest approach: tell them they need
a connection (and make sure the app doesn’t crash). Then you can analyze your app and determine
where it’s worth improving the offline experience.

16.1 How Do We Know?


If we try and can’t get data then we’ll know that they’re offline. We should show the user an
alert when that happens so they know that the data that they’re seeing isn’t live. We’ll add that
functionality to getGists in the GitHubAPIManager first. Here’s what getGists looks like now:

func getGists(urlRequest: URLRequestConvertible, completionHandler:


(Result<[Gist], NSError>, String?) -> Void) {
alamofireManager.request(urlRequest)
.validate()
.responseArray { (response:Response<[Gist], NSError>) in
if let urlResponse = response.response,
authError = self.checkUnauthorized(urlResponse) {
completionHandler(.Failure(authError), nil)
return
}
guard response.result.error == nil,
let gists = response.result.value else {
print(response.result.error)
completionHandler(response.result, nil)
return

209
What if They’re Offline? 210

// need to figure out if this is the last page


// check the link header, if present
let next = self.getNextPageFromHeaders(response.response)
completionHandler(.Success(gists), next)
}
}

Try launching the app then turning off your internet connection to see what kind of error you get
when you pull to refresh:

{Error Domain=NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline."


UserInfo={NSUnderlyingError=0x7fc8fb403940
{Error Domain=kCFErrorDomainCFNetwork Code=-1009 "(null)"
UserInfo={_kCFStreamErrorCodeKey=8, _kCFStreamErrorDomainKey=12}},
NSErrorFailingURLStringKey=https://api.github.com/gists/public,
NSErrorFailingURLKey=https://api.github.com/gists/public,
_kCFStreamErrorDomainKey=12, _kCFStreamErrorCodeKey=8,
NSLocalizedDescription=The Internet connection appears to be offline.}

Looks like we’ve got an NSURLErrorDomain error with code -1009, which translates to NSURLEr-
rorNotConnectedToInternet. So we can check for that error when we call getGists and show the
user a warning that they’re not online.
An alert view would be pretty intrusive in that case so we’ll pull in another CocoaPod that has a
nicer error display. Add BRYXBanner v0.4.1 to your project using CocoaPods and import it in the
MasterViewController:

import UIKit
import Alamofire
import PINRemoteImage
import BRYXBanner

class MasterViewController: UITableViewController, LoginViewDelegate {


...
}

Here’s how we’re currently handling errors in the loadGists:


What if They’re Offline? 211

if let error = result.error {


if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.showOAuthLoginView()
}
}

We need to add handling NSURLErrorNotConnectedToInternet errors as well as NSURLErrorUser-


AuthenticationRequired errors:

if error.domain == NSURLErrorDomain {
if error.code == NSURLErrorUserAuthenticationRequired {
self.showOAuthLoginView()
} else if error.code == NSURLErrorNotConnectedToInternet {
...
}
}

When that happens we’ll show a banner telling them that they need an internet connection. If a
banner is already showing we’ll have to hide it before showing a new one. To keep track of whether
the banner is showing we’ll need to save it as a variable:

...
import BRYXBanner

class MasterViewController: UITableViewController, LoginViewDelegate {

var detailViewController: DetailViewController? = nil


var gists = [Gist]()
var nextPageURLString: String?
var isLoading = false
var dateFormatter = NSDateFormatter()
var notConnectedBanner: Banner?

...
}

To show the banner:


What if They’re Offline? 212

guard result.error == nil else {


print(result.error)
self.nextPageURLString = nil

self.isLoading = false
if let error = result.error {
if error.domain == NSURLErrorDomain {
if error.code == NSURLErrorUserAuthenticationRequired {
self.showOAuthLoginView()
} else if error.code == NSURLErrorNotConnectedToInternet {
// show not connected error & tell em to try again when they do have a connection
// check for existing banner
if let existingBanner = self.notConnectedBanner {
existingBanner.dismiss()
}
self.notConnectedBanner = Banner(title: "No Internet Connection",
subtitle: "Could not load gists." +
" Try again when you're connected to the internet",
image: nil,
backgroundColor: UIColor.redColor())
}
self.notConnectedBanner?.dismissesOnSwipe = true
self.notConnectedBanner?.show(duration: nil)
}
}
return
}

Here’s what that’ll look like when it’s shown:


What if They’re Offline? 213

No Internet Banner

Let’s check our other API calls to make sure they’re also handling a lack of internet connection
properly. First creating new gists:

GitHubAPIManager.sharedInstance.createNewGist(description, isPublic: isPublic,


files: files, completionHandler: {
result in
guard result.error == nil, let successValue = result.value
where successValue == true else {
if let error = result.error {
print(error)
}
let alertController = UIAlertController(title: "Could not create gist",
message: "Sorry, your gist couldn't be deleted. " +
"Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(okAction)
self.presentViewController(alertController, animated:true, completion: nil)
return
}
self.navigationController?.popViewControllerAnimated(true)
})

If we wanted to we could specifically check the domain and code for that error. But we won’t bother
since we don’t have a different message that we could show for other domains and codes. At this
What if They’re Offline? 214

point the user probably needs to be alerted to the failure of the delete call so a UIAlertController
is appropriate. You could change it to a banner if you prefer that look but it’s fine as it is.

GitHubAPIManager.sharedInstance.deleteGist(id, completionHandler: {
(error) in
print(error)
if let _ = error {
// Put it back
self.gists.insert(gistToDelete, atIndex: indexPath.row)
tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Right)
// tell them it didn't work
let alertController = UIAlertController(title: "Could not delete gist",
message: "Sorry, your gist couldn't be deleted. " +
"Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(okAction)
// show the alert
self.presentViewController(alertController, animated:true, completion: nil)
}
})

Deleting is similar to creating: it makes sense to interrupt the user’s actions to tell them that it failed.
What about getting the starred status:

GitHubAPIManager.sharedInstance.isGistStarred(gistId, completionHandler: {
result in
if let error = result.error {
print(error)
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title: "Could load starred status",
message: error.description,
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true, completion: nil)
}
}

if let status = result.value where self.isStarred == nil { // just got it


self.isStarred = status
What if They’re Offline? 215

self.tableView?.insertRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})

If there isn’t an internet connection then we’ll get an error printed to the console but the user
won’t get told anything. We should probably let the user know that it’s happening. We’ll use an
orange banner instead of a red one since it isn’t a critical error. First import BRYXBanner in the
DetailViewController and add a variable for the banner:

import UIKit
import WebKit
import BRYXBanner

class DetailViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {


@IBOutlet weak var tableView: UITableView!
var isStarred: Bool?
var alertController: UIAlertController?
var notConnectedBanner: Banner?

...
}

And add creating the banner if there’s no internet connection:

func fetchStarredStatus() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.isGistStarred(gistId, completionHandler: {
result in
if let error = result.error {
print(error)
if error.domain == NSURLErrorDomain {
if error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title:
"Could not get starred status", message: error.description,
preferredStyle: .Alert)
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true,
completion: nil)
} else if error.code == NSURLErrorNotConnectedToInternet {
// show not connected error & tell em to try again when they do have a conne\
What if They’re Offline? 216

ction
// check for existing banner
if let existingBanner = self.notConnectedBanner {
existingBanner.dismiss()
}
self.notConnectedBanner = Banner(title: "No Internet Connection",
subtitle: "Can not display starred status. " +
"Try again when you're connected to the internet",
image: nil,
backgroundColor: UIColor.orangeColor())
self.notConnectedBanner?.dismissesOnSwipe = true
self.notConnectedBanner?.show(duration: nil)
}
}
}

if let status = result.value where self.isStarred == nil { // just got it


self.isStarred = status
self.tableView?.insertRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}

We also have 2 more web service calls to star and unstar gists. We opted to display alerts for those
if there’s an error since they’re actions that the user specifically requested to happen:

func starThisGist() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.starGist(gistId, completionHandler: {
(error) in
if let error = error {
print(error)
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title: "Could not star gist",
message: error.description, preferredStyle: .Alert)
} else {
self.alertController = UIAlertController(title: "Could not star gist",
message: "Sorry, your gist couldn't be starred. " +
"Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
}
What if They’re Offline? 217

// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true, completion: nil)
} else {
self.isStarred = true
self.tableView.reloadRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}

func unstarThisGist() {
if let gistId = gist?.id {
GitHubAPIManager.sharedInstance.unstarGist(gistId, completionHandler: {
(error) in
if let error = error {
print(error)
if error.domain == NSURLErrorDomain &&
error.code == NSURLErrorUserAuthenticationRequired {
self.alertController = UIAlertController(title: "Could not unstar gist",
message: error.description, preferredStyle: .Alert)
} else {
self.alertController = UIAlertController(title: "Could not unstar gist",
message: "Sorry, your gist couldn't be unstarred. " +
"Maybe GitHub is down or you don't have an internet connection.",
preferredStyle: .Alert)
}
// add ok button
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
self.alertController?.addAction(okAction)
self.presentViewController(self.alertController!, animated:true, completion: nil)
} else {
self.isStarred = false
self.tableView.reloadRowsAtIndexPaths(
[NSIndexPath(forRow: 2, inSection: 0)],
withRowAnimation: .Automatic)
}
})
}
}
What if They’re Offline? 218

Now we can play with the app a bit and see if we find any bugs. Try turning your internet connection
on and off to find any issues.
I found two: First, if there’s a red banner showing then we select a gist we’ll see the orange banner.
But if we dismiss the orange banner then the red one is still shown. We should dismiss the banner
when we change views:

override func viewWillDisappear(animated: Bool) {


if let existingBanner = self.notConnectedBanner {
existingBanner.dismiss()
}
super.viewWillDisappear(animated)
}

We’ll need to add that code to both the MasterViewController and the DetailViewController.
The second issue happens if we lose our internet connection and we switch lists of gists. Then we
can end up with the wrong list showing. To fix that we’ll clear out the list of gists when they select
a different list in the segmented control:

@IBAction func segmentedControlValueChanged(sender: UISegmentedControl) {


// only show add/edit buttons for my gists
...

// clear gists so they can't get shown for the wrong list
self.gists = [Gist]()
self.tableView.reloadData()

loadGists(nil)
}

There’s just one more bit of internet dependent functionality in our app: logging in with OAuth 2.0.
To test that feature you’ll have to reset the simulator using Simulator -> Reset Content and Services
or uninstall the app from your device to simulate the initial run of the app.
When we test that feature out we’ll find that the Login view controller just keeps popping up when
we tap the login button. That’s a pretty awful experience for the user, especially if this is the very
first time they’ve run the app. Our code is detecting the lack of internet connection correctly using
the SFSafariViewControllerDelegate method:
What if They’re Offline? 219

func safariViewController(controller: SFSafariViewController, didCompleteInitialLoad


didLoadSuccessfully: Bool) {
// Detect not being able to load the OAuth URL
if (!didLoadSuccessfully) {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")
if let completionHandler =
GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler {
let error = NSError(domain: NSURLErrorDomain, code:
NSURLErrorNotConnectedToInternet,
userInfo: [NSLocalizedDescriptionKey: "No Internet Connection",
NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(error)
}
controller.dismissViewControllerAnimated(true, completion: nil)
}
}

But the completion handler is set up to just try again no matter what the error is:

GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler = { (error) -> Void in


self.safariViewController?.dismissViewControllerAnimated(true, completion: nil)
if let error = error {
print(error)
self.isLoading = false
// TODO: handle error
// Something went wrong, try again
self.showOAuthLoginView()
} else {
self.loadGists(nil)
}
}

Let’s improve that using our banners:


What if They’re Offline? 220

GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler = { (error) -> Void in


self.safariViewController?.dismissViewControllerAnimated(true, completion: nil)
if let error = error {
print(error)
self.isLoading = false
if error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet {
// show not connected error & tell em to try again when they do have a connection
// check for existing banner
if let existingBanner = self.notConnectedBanner {
existingBanner.dismiss()
}
self.notConnectedBanner = Banner(title: "No Internet Connection",
subtitle: "Could not load gists. Try again when you're connected to the internet",
image: nil,
backgroundColor: UIColor.redColor())
self.notConnectedBanner?.dismissesOnSwipe = true
self.notConnectedBanner?.show(duration: nil)
} else {
// Something went wrong, try again
self.showOAuthLoginView()
}
} else {
self.loadGists(nil)
}
}

If we test that we’ll find the Login view controller is still getting popped up because it’s set to always
show up when we show the Master view controller but aren’t logged in. The easy way to fix that is to
leave the app thinking it’s still loading an OAuth token when we don’t have an internet connection:

func safariViewController(controller: SFSafariViewController, didCompleteInitialLoad


didLoadSuccessfully: Bool) {
// Detect not being able to load the OAuth URL
if (!didLoadSuccessfully) {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setBool(false, forKey: "loadingOAuthToken")
if let completionHandler =
GitHubAPIManager.sharedInstance.OAuthTokenCompletionHandler {
let error = NSError(domain: NSURLErrorDomain,
code: NSURLErrorNotConnectedToInternet, userInfo: [
NSLocalizedDescriptionKey: "No Internet Connection",
NSLocalizedRecoverySuggestionErrorKey: "Please retry your request"])
completionHandler(error)
}
controller.dismissViewControllerAnimated(true, completion: nil)
What if They’re Offline? 221

}
}

Now the app won’t pop up the Login view controller again until we pull to refresh.

Analyze and test your app for web service calls that could fail. Make sure each one is
handled with an experience that will be acceptable to the user and that they’ll understand
what’s happening. Add banners and alerts as appropriate.

We’ve handled all of the issues with lack of network connection so Apple won’t be rejecting our app
for that. But what if we wanted to provide a better experience, by letting users look at the gists they
previously loaded while they’re offline? We’ll do that next.
If you got tired of typing, here’s the code: (tagged “noInternet”)¹.

16.2 Save a Local Copy


For a simple app like ours it’s probably good enough to show them that they’re offline and display
the latest data that they loaded in a read-only mode. So we’d need to persist the lists of gists. Without
an internet connection they wouldn’t be able to delete gists or star/unstar gists or see whether they
previously starred a gist. But we already handled those cases earlier in this chapter.
NSKeyedArchiver can be used to easily serialize objects so they can be written to disk. It works out
of the box for types like arrays and strings but we’ll have to let it know how to deal with our custom
classes. The two classes that we’ll need to add that support to are Gist and File.
To support that archiving our classes need to conform to the NSCoding protocol, which in turn
requires the NSObject protocol:

class Gist: NSObject, NSCoding, ResponseJSONObjectSerializable {


...
}

The NSObject protocol requires that we change the declaration of the existing init function to:

required override init() {


}

It also includes a description property so we’ll change ours to gistDescription:

¹https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/noInternet
What if They’re Offline? 222

class Gist: NSObject, NSCoding, ResponseJSONObjectSerializable {


var id: String?
var gistDescription: String?
...

required init(json: JSON) {


self.gistDescription = json["description"].string
...
}

...
}

And we’ll have to change the other view controllers to use gistDescription. The easiest way to
find what needs to be changed is to search for .description.
In the MasterViewController:

cell.textLabel!.text = gist.gistDescription

And in the DetailViewController:

func tableView(tableView: UITableView, cellForRowAtIndexPath


indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)

if indexPath.section == 0 {
if indexPath.row == 0 {
cell.textLabel?.text = gist?.gistDescription

The NSCoding protocol requires 2 functions, one to encode the object and the other to create an
object by decoding it:

class Gist: NSObject, NSCoding, ResponseJSONObjectSerializable {


...

// MARK: NSCoding
@objc func encodeWithCoder(aCoder: NSCoder) {
...
}

@objc required convenience init?(coder aDecoder: NSCoder) {


self.init()
What if They’re Offline? 223

...
}
}

We need to include each property in those functions using the functions available on NSCoder to
encode and decode each property:

@objc func encodeWithCoder(aCoder: NSCoder) {


aCoder.encodeObject(self.id, forKey: "id")
aCoder.encodeObject(self.gistDescription, forKey: "gistDescription")
aCoder.encodeObject(self.ownerLogin, forKey: "ownerLogin")
aCoder.encodeObject(self.ownerAvatarURL, forKey: "ownerAvatarURL")
aCoder.encodeObject(self.url, forKey: "url")
aCoder.encodeObject(self.createdAt, forKey: "createdAt")
aCoder.encodeObject(self.updatedAt, forKey: "updatedAt")
if let files = self.files {
aCoder.encodeObject(files, forKey: "files")
}
}

@objc required convenience init?(coder aDecoder: NSCoder) {


self.init()

self.id = aDecoder.decodeObjectForKey("id") as? String


self.gistDescription = aDecoder.decodeObjectForKey("gistDescription") as? String
self.ownerLogin = aDecoder.decodeObjectForKey("ownerLogin") as? String
self.ownerAvatarURL = aDecoder.decodeObjectForKey("ownerAvatarURL") as? String
self.createdAt = aDecoder.decodeObjectForKey("createdAt") as? NSDate
self.updatedAt = aDecoder.decodeObjectForKey("updatedAt") as? NSDate
if let files = aDecoder.decodeObjectForKey("files") as? [File] {
self.files = files
}
}

And the same for File:


What if They’re Offline? 224

class File: NSObject, NSCoding, ResponseJSONObjectSerializable {


var filename: String?
var raw_url: String?
var content: String?

...

// MARK: NSCoding
@objc func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(self.filename, forKey: "filename")
aCoder.encodeObject(self.raw_url, forKey: "raw_url")
aCoder.encodeObject(self.content, forKey: "content")
}

@objc required convenience init?(coder aDecoder: NSCoder) {


let filename = aDecoder.decodeObjectForKey("filename") as? String
let content = aDecoder.decodeObjectForKey("content") as? String

// use the existing init function


self.init(aName: filename, aContent: content)
self.raw_url = aDecoder.decodeObjectForKey("raw_url") as? String
}
}

Now we can save the gists but we need to implement actually doing so. Create a new PersistenceM-
anager.swift file that will be responsible for handling the saving and loading. We’ll keep it generic
by set up saving and loading arrays instead of specifying that they’re gists:

import Foundation

class PersistenceManager {
class func saveArray<T: NSCoding>(arrayToSave: [T], path: Path) {
// TODO: implement
}

class func loadArray<T: NSCoding>(path: Path) -> [T]? {


// TODO: implement
}
}

We’ll need a spot to save the gists. There’s a documents directory that will work fine:
What if They’re Offline? 225

class PersistenceManager {
class private func documentsDirectory() -> NSString {
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,
.UserDomainMask, true)
let documentDirectory = paths[0] as String
return documentDirectory
}
...
}

We’ll need to specify a different place to save each list of gists so they don’t overwrite each other.
Let’s use an enum for that. We could add more items later if our app needed to save other objects:

enum Path: String {


case Public = "Public"
case Starred = "Starred"
case MyGists = "MyGists"
}

class PersistenceManager {
...
}

Ok, now let’s implement saving. We’ll get the path to save it to then use NSKeyedArchiver.archiveRootObject
to save the array to that path:

class func saveArray<T: NSCoding>(arrayToSave: [T], path: Path) {


let file = documentsDirectory().stringByAppendingPathComponent(path.rawValue)
NSKeyedArchiver.archiveRootObject(arrayToSave, toFile: file)
}

Loading the array is pretty similar:

class func loadArray<T: NSCoding>(path: Path) -> [T]? {


let file = documentsDirectory().stringByAppendingPathComponent(path.rawValue)
let result = NSKeyedUnarchiver.unarchiveObjectWithFile(file)
return result as? [T]
}

So all together here’s our PersistenceManager:


What if They’re Offline? 226

import Foundation

enum Path: String {


case Public = "Public"
case Starred = "Starred"
case MyGists = "MyGists"
}

class PersistenceManager {
class private func documentsDirectory() -> NSString {
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,
.UserDomainMask, true)
let documentDirectory = paths[0] as String
return documentDirectory
}

class func saveArray<T: NSCoding>(arrayToSave: [T], path: Path) {


let file = documentsDirectory().stringByAppendingPathComponent(path.rawValue)
NSKeyedArchiver.archiveRootObject(arrayToSave, toFile: file)
}

class func loadArray<T: NSCoding>(path: Path) -> [T]? {


let file = documentsDirectory().stringByAppendingPathComponent(path.rawValue)
let result = NSKeyedUnarchiver.unarchiveObjectWithFile(file)
return result as? [T]
}
}

When should we save the gists? As soon as they’re loaded makes sense to me. We’ll add a call to
PersistenceManager.saveArray in loadGists after getting the correct path:

if let fetchedGists = result.value {


if let _ = urlToLoad {
self.gists += fetchedGists
} else {
self.gists = fetchedGists
}
let path:Path
if self.gistSegmentedControl.selectedSegmentIndex == 0 {
path = .Public
} else if self.gistSegmentedControl.selectedSegmentIndex == 1 {
path = .Starred
} else {
path = .MyGists
}
What if They’re Offline? 227

PersistenceManager.saveArray(self.gists, path: path)


}

And then we can load them if we don’t have an internet connection when we show the banner:

if error.code == NSURLErrorUserAuthenticationRequired {
self.showOAuthLoginView()
} else if error.code == NSURLErrorNotConnectedToInternet {
let path:Path
if self.gistSegmentedControl.selectedSegmentIndex == 0 {
path = .Public
} else if self.gistSegmentedControl.selectedSegmentIndex == 1 {
path = .Starred
} else {
path = .MyGists
}
if let archived:[Gist] = PersistenceManager.loadArray(path) {
self.gists = archived
} else {
self.gists = [] // don't have any saved gists
}

// show not connected error & tell em to try again when they do have a connection
...
}

Save and run. After you’ve loaded some gists, turn the internet off and make sure you can still see
them. Relaunch the app with the internet still off and they should still show up with the red banner
shown.

See if there are any parts of your app that make sense for read-only offline support. If so,
use NSKeyedArchiver to save and load those items to the device when you get them so
users can see them offline.

If you got tired of typing, here’s the code: (tagged “persistent”)².

16.3 Databases
If your app is more complex then you’ll probably want a real database. There are whole books
written on iOS databases. Along with keeping your data in a database comes syncing issues. It’s not
²https://github.com/cmoulton/grokSwiftREST_v1.1/releases/tag/persistent
What if They’re Offline? 228

too bad if the data is only accessed by a single user but if multiple users can modify objects then
you’ll have to deal with conflicts between their changes when one of them didn’t have the latest
version.
Databases don’t inherently fix the issues that come with not having an internet connection. But they
do make it easier to handle complicated relationships between objects and large amounts of data.
If you need to go down this path take a look at Core Data. It’s built-in to iOS and does a lot more
than simple database functions. You will need to monitor the internet connection and sync up when
you can.
Realm³ is becoming popular as an alternative. If you want to you can use SQLite (which is actually
what Core Data uses under the hood).
If you’re building your whole app from scratch, including the back end, you might want to consider
services like Parse⁴ or Kinvey⁵. They offer SDKs that include features to manage offline use.

Consider just how rich your app’s offline experience needs to be. Consider using a database
if users should be able to perform lots of tasks offline and sync up with the back-end later.

³https://realm.io
⁴http://blog.parse.com/learn/parse-local-datastore-for-ios/
⁵http://devcenter.kinvey.com/ios/guides/caching-offline
17. What Next?
We’ve built a working prototype of a REST API-backed iOS app in Swift. Before it’s ready for the
App Store it’ll need a few things. Each of these items merits a book on its own so you should seek
other sources to make sure you’ve covered them adequately before submitting your app:

17.1 User Interface


We’ve been neglecting the user interface throughout this book. Before releasing at least read through
the Apple iOS Human Interface Guidelines¹. Not following the HIG is cause for an App Store
rejection.
Beyond the HIG you’ll probably want to make the app a lot prettier too. If you don’t have a designer
check out the numerous UIKits available online to get away from completely standard styles. Don’t
go too far from the norm though, users want something familiar enough that they know how to
interact with it.

17.2 Test the User Experience


Give it to someone, ask them to do one of the tasks and silently watch them try. If they can’t easily
figure it out then you should strongly consider redesigning the app to make that task more obvious.
Try this test with a few people, preferably not your friends that you’ve already told all about the
app.

17.3 Suggested Exercises


If you want to keep working on the demo app here are a few ideas. Read the GitHub Gists API docs²
for details:

• Make it look great


• Make the file display a lot better. You can use the content property but you’ll need to handle
truncation since you’ll only get the first MB of data for each file
• Let the user navigate to the GitHub web page from a gist or file in the app
¹https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/
²https://developer.github.com/v3/

229
What Next? 230

• Add filters to let users search the gists. Unfortunately there isn’t any support for searching in
the API but you can let users filter or search the gists that are loaded to their device
• Add an edit mode to the list of starred & public gists to star/unstar multiple gists at a time
• Extend the gist creation form to allow multiple files
• Let users edit their existing gists
• Build out user profiles so users can view details about the writers of the gists

17.4 Did I Miss Anything?


One of the best things about ebooks is that cats can’t shred them. Another is that they can be updated,
just like this one was when Alamofire v3 came out. So while the book is fresh in your mind email
me right now at christina+book@teakmobile.com³ and tell me one way it could be better.
If you’re looking for more Swift tutorials, I write them about once a week at Grok Swift⁴.

³mailto:christina+book@teakmobile.com
⁴https://grokswift.com
A Brief Introduction to CocoaPods
If you’re not familiar with CocoaPods, it’s worth taking a few minutes to learn about the lovely
dependency manager commonly used for iOS libraries today.
Cocoapods is great for adding libraries to your iOS projects, in Objective-C and Swift. In fact, it’s
easy to use Objective-C code in iOS Swift projects. If you’re curious, check out Objective-C in Swift
Project⁵.
We’ll just cover the simple basics that we’ll use throughout this book so that when I say “add the
Alamofire v3.1 CocoaPod to your project” we don’t need to spend a few paragraphs detailing how
to do that.

Adding a CocoaPod to a Project


Let’s say we’re going to add the SwiftyJSON CocoaPod to an Xcode project. Here’s what we need
to do:
Close Xcode
Open the terminal in the project top directory (the directory with the .xcodeproj file for your project).
If you haven’t previously installed CocoaPods run:

sudo gem install cocoapods

If you need more info on installing CocoaPods, check out their Getting Started guide⁶.
Once it’s done installing CocoaPods, you need to initialize Cocoapods for your project. So run:

pod init

That will create a new file called “Podfile” (to be honest, I think that’s all it does). Using a text editor
open the newly created Podfile and replace the contents with:

⁵https://grokswift.com/objective-c-in-swift/
⁶https://guides.cocoapods.org/using/getting-started.html

231
A Brief Introduction to CocoaPods 232

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!
pod 'SwiftyJSON'

Save the Podfile then switch back to Terminal and run:

pod install

Open the .xcworkspace file in Xcode. Navigate back to whatever class you want to use SwiftyJSON
in and add “import SwiftyJSON” at the top like this:

import Foundation
import SwiftyJSON

class MyClass {
...
}

Now you can use SwiftyJSON in that file.

What Does the Podfile Mean?


While it’s nice to just get instructions that work, it’s usually a good idea to know why they work.
So let’s take a look at that Podfile:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!
pod 'SwiftyJSON'

When you run pod install CocoaPods looks for a Podfile and tries to install the pods listed in it.
Install in this case means “download and add to the Xcode project”. A pod is generally a library,
really just a chunk of code that you want to use in your project.
Let’s go through it line by line:

source 'https://github.com/CocoaPods/Specs.git'

The first line tells CocoaPods where on the internet to find pods. We’re using the default public
CocoaPods repository which is a great place to check for libraries for your projects.
A Brief Introduction to CocoaPods 233

platform :ios, '9.0'

The second line specifies that we’re working on an app for iOS (not OS X) and we’re building an
app for the iOS 9.0 SDK. Including this info in a Podfile means that pods can have different version
for iOS and OS X as well as for different versions of the iOS SDK.

use_frameworks!

use_frameworks! tells CocoaPods how we want to integrate the code libraries with our project. In
Swift we want it to wrap up the code in a framework then include the framework in our project.
Since CocoaPods pre-dates Swift, that’s not the default so we have to include this line. Want more
details? See the release notes for CocoaPods v0.36⁷.

pod 'SwiftyJSON'

And finally we specify which pod (or pods) we want to install.

Other Options
You can do some neat stuff with CocoaPods including adding different code to testing and App Store
versions of your app or making private CocoaPods for use within a team. If you’re curious, check
out Creating and Using CocoaPods by Jeffrey Sambells⁸.

Dependencies
The real time saver in CocoaPods is that pods can specify dependencies. So if SwiftyJSON required
some other library then CocoaPods would make sure we have it in our Pods before downloading
SwiftyJSON and adding it to our project. It’ll also make sure that we have the correct compatible
version. So we don’t need to hunt down and install a bunch of prerequisites before installing a pod.

CocoaPods Version Numbers


One option that we’ll use in CocoaPods is to specify which version of a pod we want. We can specify
an exact number or a less specific number. For example to use v2.3.0:

⁷http://blog.cocoapods.org/CocoaPods-0.36/
⁸http://jeffreysambells.com/talks/2014/01/23/using-and-creating-cocoapods
A Brief Introduction to CocoaPods 234

pod 'SwiftyJSON', '2.3.0'

We could also say that we want to use SwiftyJSON v2.3.whatever to get small updates:

pod 'SwiftyJSON', '~> 2.3.0'

Which would allow v2.3.0, 2.3.1, 2.3.2, … but not v2.4.


Or even v2.whatever:

pod 'SwiftyJSON', '~> 2.3'

Which would allow v2.3, 2.4, … but not v3.0.


If we leave off the version number then CocoaPods will just install the latest version.

Updating CocoaPods
Unless you tell it to, CocoaPods won’t auto-update to newer versions. To tell CocoaPods that you
do want newer versions (if your version numbers will allow it) run:

pod update

You’ll see a message in the terminal showing you which pods were updated and to which versions.
You’ll also need to run that command if you change the version numbers in the Podfile or add more
pods to it.

Вам также может понравиться