1. Trang chủ
  2. » Công Nghệ Thông Tin

iOS apps with REST APIs

241 670 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 241
Dung lượng 9 MB

Nội dung

let task = session.dataTaskWithRequesturlRequest, completionHandler: { data, response, error in guard let responseData = data else { print "Error: did not receive data" return } guard

Trang 2

Building 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 3

Please 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 4

Thanks i

Trang 5

6 Custom Headers 68

12 Switching Between View Controllers and More JSON Parsing 161

Trang 6

12.3 Configuring the Detail View Controller 166

Trang 7

Without 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 8

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 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 10

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 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 11

Version 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 12

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

Mashape is a trademark of Mashape, Inc registered in the United States

Trang 13

It’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 14

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 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 15

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

it implemented

Trang 16

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 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 18

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 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 19

Put 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 20

Calls 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 21

The 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 22

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 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 23

let 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 24

let 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 25

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

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

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 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 27

let 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 28

let 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 29

let 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 30

Just 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 31

Let’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 32

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 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 33

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 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 34

let 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 37

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:

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 38

var 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 39

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:

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 40

Save 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

Ngày đăng: 18/04/2017, 10:36

TỪ KHÓA LIÊN QUAN

w