let task = session.dataTaskWithRequesturlRequest, completionHandler: { data, response, error in guard let responseData = data else { print "Error: did not receive data" return } guard
Trang 2Building Web-Driven Apps in Swift
©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
Trang 3Please help Christina Moulton by spreading the word about this book onTwitter!
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 thishashtag on Twitter:
https://twitter.com/search?q=#SwiftRestAppsBook
Trang 4Thanks i
Trang 56 Custom Headers 68
12 Switching Between View Controllers and More JSON Parsing 161
Trang 612.3 Configuring the Detail View Controller 166
Trang 7Without a few key people this book wouldn’t have happened Most of all, thanks to Jeff Moultonfor putting up with my excessive focus on coding & writing, even while living on a 34’ sailboat Jeffalso took the cover photo.
Thanks also to:
• My Twitter peeps for support & fav’s
• @BugKrusha¹& theiOS Developers²community
Trang 8You 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 mathematicalanalysis offlatMap Only the nitty gritty that you need to get real work done now: pulling data fromyour 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 foryou.
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 modelobjects
• 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 anotherweb 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 userssubmit 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 toyour app We’ll start by figuring out how to make API calls in Swift then we’ll start building out ourapp 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
Trang 9• 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 onthe 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 expandtheir 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 oriented programming language or have completed several intro to iOS tutorials
object-• Designers, managers, UX pros, … It’s a programming book All the monospace font insertswill probably drive you crazy
• Cross-platform developers dedicated to their tools (including HTML5 & Xamarin), this is allSwift & 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 learnbest and how urgently you need to implement your own app, there are two different approaches youmight take:
1 Work through the tutorials as written, creating an app for GitHub Gists You’ll understandhow 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 thetext I’ll point out where you’ll need to analyze your own requirements and API to help youfigure out how to modify the example code to work with your API Those tips will look likethis:
Trang 10List 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 whatwe’re going to build Then we’ll start building the gists app, right after an introduction to makingnetwork calls and parsing JSON in Swift
1.5 What We Mean By Web Services / APIs / REST /
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 onehas its own quirks Most of the integration is similar enough that we can generalize how to integratethem 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 thesedays 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 JSONparsing 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 atNSXMLParser²
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’llexplicitly list the versions used The most commonly used ones are Alamofire 3.1 and SwiftyJSON2.3
¹https://en.wikipedia.org/wiki/Representational_state_transfer
²https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSXMLParser_Class/
Trang 11Version 1.0 of this book used Alamofire 2.0 and SwiftyJSON 2.2 Changes to the code betweenversions 1.0 and this version can be found onGitHub³.
1.8 Source Code
All sample code is availableon GitHub⁴under theMIT license⁵ Links are provided throughout thetext Each chapter has a tag allowing you to check out the code in progress up to the end of thatchapter
Individuals are welcome to use code for commercial and open-source projects As a courtesy, pleaseprovide 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 authorhas made every effort to ensure the accuracy of the information within this book was correct attime of publication Teak Mobile Inc and/or Christina Moulton do not assume and hereby disclaimsany liability to any party for any loss, damage, or disruption caused by errors or omissions, whethersuch 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 anyother commercial damage, including but not limited to special, incidental, consequential, or otherdamages
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, orservice marks of their respective holders They are used throughout this book in an editorial fashiononly In addition, terms suspected of being trademarks, registered trademarks, or service marks havebeen appropriately capitalized, although Teak Mobile Inc and Christina Moulton cannot attest tothe accuracy of this information Use of a term in this book should not be regarded as affecting thevalidity of any trademark, registered trademark, or service mark Teak Mobile Inc and/or ChristinaMoulton 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
Trang 12GitHub is a trademark of GitHub, Inc registered in the United States.
Mashape is a trademark of Mashape, Inc registered in the United States
Trang 13It’s always tempting to jump right into coding but it usually goes a lot smoother if we plan it out inadvance At the least we need some idea of what we’re building Let’s lay that out for the gists appand 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 thendesign 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 allthe 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 thefirst 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 mighteven 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
Trang 142.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 getthe data you’ll need to display We’ll check the documentation for theGitHub gists API¹to find theendpoint 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 seemore 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 wedon’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/
Trang 15GET / 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
it implemented
Trang 162.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 eachtask and figure out how we’d like it to work I’ve reordered the tasks below a bit to group togetherbits that will share parts of the interface
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 listdetails about the gist (description and filenames) and let them view the text of the files It’ll alsoshow 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 agist in that view
Trang 17• 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 accomplishthose tasks
GitHub Gists API docs²
So we’ll need to set up authentication, preferably OAuth 2.0, including thegistscope The API willwork with a username/password but then we’d have to worry about securing that data With OAuth2.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 theauth chapterwe’ll cover how to plement OAuth 2.0, token-based authentication, and basic auth with username/password
im-²https://developer.github.com/v3/gists/#authentication
Trang 182.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 fortransferring data and it’s pretty picky about just how it’s implemented Sadly this means that alot of servers out there don’t meet ATS’s requirements GitHub’s gist API complies with the ATSrequirements 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 probablyneed to add an exception to ATS See theNetworking 101 chapterfor details on adding thatexception 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 theapp 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
Trang 19Put your views and tasks in order to implement them Try to match up roughly with theorder for the gists app If you don’t have an API call to start with that doesn’t requireauthentication you might need to jump ahead to theauth chapterbefore starting on the
table view chapter If your API requires custom headers to be sent with all requests thenyou’ll want to start with theheaders chapterthen come back to thetable view chapter
Now that we’ve sorted out the basic requirements for our app we know where to start First we’llspend a little time looking at how to make web requests and parse JSON in Swift so we don’t getbogged down with those details later
Trang 20Calls 101
I was all ready to jump right in to some useful code for you then Apple introducedApp TransportSecurity¹in iOS 9 While ATS should be a great feature for securing the data being sent to and fromyour 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 theNetworking 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 disableATS 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 athttp://jsonplaceholder.typicode.com/²so that’s whatwe’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 adictionary for the server: jsonplaceholder.typicode.com (note: no trailing slashes and no http
or https prefix) Within thejsonplaceholder.typicode.comdictionary we’ll have a boolean entryNSThirdPartyExceptionAllowsInsecureHTTPLoadsset toYES:
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 bookwe’ll mostly use Alamofire³, a rich networking library, but you can also use NSURLSessions’sasynchronous 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
Trang 21The function to use to make an async URL request is:
public func dataTaskWithRequest (request: NSURLRequest ,
completionHandler: ( NSData ?, NSURLResponse ?, NSError ?) -> Void )
The simplest case is a GET request Of course, we need an API to hit Fortunately there’s super handy
we can see that the id for the first post is 1 So let’s grab it:
First, set up the URL request:
guard let url = NSURL (string: postEndpoint) else {
print ( "Error: cannot create URL" )
return
}
Theguardstatement lets us check that the URL we’ve provided is valid
Then we need an NSURLSession to use to send the request:
Then create the data task:
⁴https://github.com/typicode/jsonplaceholder
⁵http://jsonplaceholder.typicode.com/posts/
Trang 22let 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 requestsince that’s the default) To actually get the results to do anything useful we need to implement thecompletion 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 notused 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 littlewhile, like making an API call, and you need to do something when that task is done, like updatingthe UI to show the data You’ll see completion handlers in Apple’s APIs likedataTaskWithRequestand later on we’ll add some of our own completion handlers when we’re building out our API calls
IndataTaskWithRequestthe 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 butit’s not necessary because the compiler can figure it out Sometimes it’s good to remember thatpeople read code, not just computers, so it doesn’t hurt to be explicit:
Trang 23let task = session.dataTaskWithRequest(urlRequest, completionHandler: { (data: NSData ?, response: NSURLResponse ?, error: NSError ?) in
// this is where the completion handler code goes
let task = session.dataTaskWithRequest(urlRequest) { (data, response, error) in
// this is where the completion handler code goes
let task = session.dataTaskWithRequest(urlRequest) { (data, _, error) in
// can't do print(response) since we don't have response
to handle any of them failing similarly
Here’s how you can use a variable for a completion handler:
Trang 24let myCompletionHandler: ( NSData ?, NSURLResponse ?, NSError ?) -> Void = {
(data, response, error) in
// this is where the completion handler code goes
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 indataTaskWithRequest
In fact, there are probably a few calls like that for handling success and error cases The completionhandler will just sit around waiting to be called wheneverdataTaskWithRequestis 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 potentialerrors so we can make sure our API call worked Let’s go back to ourdataTaskWithRequestexampleand 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 anerror (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
Trang 252 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
// 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)
Trang 26The 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 doit
If you need a method type other than GET then you’ll need to use a mutableNSURLRequestso youcan set the method type:
postsUrlRequest.HTTPMethod = "POST"
Then we can set the new post as theHTTPBodyfor the request:
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 numberassigned 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 onyour 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
Trang 27let newPost: NSDictionary = [ "title" : "Frist Psot" , "body" : "I iz fisrt" , "userId" : 1 ];
do {
postsUrlRequest.HTTPBody = jsonPost
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
// 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
Trang 28let firstPostEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
firstPostUrlRequest.HTTPMethod = "DELETE"
let task = session.dataTaskWithRequest(firstPostUrlRequest, completionHandler: {
(data, response, error) in
guard let _ = data else {
print ( "error calling DELETE on /posts/1" )
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 verbosefast) or useSwiftyJSON⁶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 orpost["id"]is nil, thenpostIDwill be nil..intreturns an optional likeInt? If you’re sure the value won’t be nil then use.intValueinstead 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
Trang 29let title = myJSON[ "title" ].string
let cost = myJSON[ "cost" ].double
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:
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:
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. questworks just fine for simple cases, like a URL shortener But these days lots of apps have tons
dataTaskWithRe-of web service calls that are just begging for better handling: a higher level dataTaskWithRe-of abstraction, concisesyntax, simpler streaming, pause/resume, progress indicators, …
In Objective-C, this was a job forAFNetworking⁹ In Swift,Alamofire¹⁰is our option for elegance.While we’re harping on elegance, that JSON parsing was pretty ugly Optional binding (as of Swift1.2) helps fix that butSwiftyJSON¹¹will really help clean up that boilerplate
Trang 30Just like in previous section, we’ll use the super handyJSONPlaceholder¹²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 ):
guard let url = NSURL (string: postEndpoint) else {
print ( "Error: cannot create URL" )
return
}
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
// 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
Trang 31Let’s see how this looks with Alamofire library that I keep talking up First add Alamofire v3.1 toyour project using CocoaPods (SeeA Brief Introduction to CocoaPodsif you’re not sure how) Thenset up the request:
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 toNSURLto wrap up the string) We explicitly say it’s a GETrequest (instead of NSURLRequestassuming it)..GETis a member of theAlamofire.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 anNSHTTPURLResponse),.responsePropertyList, or.responseString(for a string) We couldeven chain multiple.responseXmethods for debugging:
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
Trang 32post[ "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 levelsunwrapping 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 {
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:
Trang 33let 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 POST, we just need to change the HTTP method and provide the post data:
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
Trang 34let post = JSON(value)
print ( "The post is: " + post.description)
}
}
And DELETE is nice and compact:
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 codeon GitHub¹³
So that’s one step better on our journey to nice, clean REST API calls But we’re still interacting withuntyped JSON which can easily lead to errors Next we’ll take another step towards a class for ourpost 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 overkillfor those simple calls we can improve our code by using an Alamofire router The router will composethe 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 APIcalls) 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 usingJSONPlaceholder¹⁴.JSONPlaceholder is a fake online REST API for testing and prototyping It’s like image placeholdersbut for web developers
Here’s our previous code:
¹³https://gist.github.com/cmoulton/01fdd4fe2c2e9c8195e1
¹⁴https://github.com/typicode/jsonplaceholder
Trang 35// Get first post
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 {
// Create new post
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)
}
}
Trang 36// Delete first post
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" )
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 Aconvenient feature of Swift enums is that the cases can have arguments For example, our.Getcasecan have anIntargument 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 theNSMutableURLRequest, 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 )
Trang 37Alamofire.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:
The.POSTcall is similar Change:
Alamofire.request(.POST, postsEndpoint, parameters: newPost, encoding: JSON)
And for the.DELETEcall, change:
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 likeRouter.Delete(1)give us anNSMutableURLRequestthatAlamofire.Request()knows how to use
We’ve defined the Router as an enum with a case for each of our 3 calls So within ourURLRequestcomputed property we can use those 3 cases For example, we can use a switch statement to definethe HTTP methods for each case:
Trang 38var method: Alamofire.Method {
And similarly we can create theNSMutableURLRequestin a switch statement:
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) !
Appending the path components from the result switch statement:
Then creating a URL request including the encoded parameters: (encoding.encode( ) handlesnil parameters just fine so we don’t need to check for that):
Trang 39let 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:
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 (encodedRequest, _) = encoding.encode(URLRequest, parameters: result.parameters) encodedRequest.HTTPMethod = method.rawValue
return encodedRequest
}
Trang 40Save and test out our code The console log should display the same post titles and lack of errorsthat we had in the previous section We’ve created a simple Alamofire router that you can adapt toyour API calls.
Here’s the example codeon GitHub¹⁵
3.4 Strongly Typed GET and POST Calls with Alamofire
We’ve usedAlamofire¹⁶to make some REST requests to a web service Now let’s clean that up bybuilding a higher layer of abstraction by mapping the JSON to a strongly typed class That’ll keepour 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 itsown file to represent the Post objects It will have a few properties, an initializer to create new Postobjects, and a description function to print out all of the properties, which is handy for debugging:
class Post {
var id: 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 theHTTP method and the URL, plus any parameters or headers We don’t need to make any changessince the router still works in terms of url requests and JSON It doesn’t need to know anythingabout our Post objects
Create a new filePostRouter.swiftfor our router:
¹⁵https://github.com/cmoulton/grokRouter
¹⁶https://github.com/Alamofire/Alamofire