- Intro
- Readings
- Resources
- Objectives
- APIs in Concept
- APIs, What Are They?
- Why Do APIs Matter
- JSON (a deeper dive)
- API Documentation
- Trying out an API
- Working with Requests
- Postman
- Testing API Requests and myjson
- The RandomUserAPI
- iFacesterGram (Project)
- iFacesterGram: Concept to Demo
- Designing the
User
Model - Coding the
APIRequestManager
- Parsing Data in Our Model
- Pull-to-Refresh
- Exercises
- This is a very quick and helpful video (must watch)
- This is a slightly longer, practical look at REST API's (must watch)
- If you understood the two videos above this link, you don't really have to read this article
- Postman - Free tool to test API requests
- Random User API - Simple API useful for simple user creation
- Google Geocoding API: Intro
- Introduce the concept of web-based APIs along with examples of popular webservices
- Frame using APIs as being able to develop amazing apps that would not be possible on your own or without large development teams
- Further dive into JSON by exploring Google Geocode responses
- Explore API documentation to understand how an API defines its inputs and outputs
- Introduce Postman as a utility for quickly testing APIs
- Begin working on iFacesterGram
How many weather apps do you think there are in the app store?
Iโd wager there must be in the thousands, at least. Some of those apps have entire teams dedicated to building the app, while some of them are a single person; a person who might have just done it as a weekend project. And despite the great variety of apps made, all of their data is (relatively) consistent.
But where are those developer teams, large and small, getting their weather data? I mean here you have 1000โs of different apps and they all have the same data set. How are they able to all present the same information?
The answer: from an API (Application Programming Interface)
An API is a digital middleman - delivering data from some service as long as you ask for it in the right way. In this case, all of these weather apps are asking a weather-related API for data on the weather. In a broad sense an API is a standard set of requests and responses that allows software to communicate. And it is not just iOS apps that use API's: websites use them, Android devices use them, and computers use them.
When iOS developers talk about API's we usually mean one of two things:
- Any library used to do a specific function in an app (for example UIKit could be considered a native iOS API for UI elements)
- A REST API that is used to communicate with some service on the web, (like a weather API!)
They allow you to develop software faster! The existance of APIs let you perform a variety of tasks that just take way too long to write out yourself. Can you imagine having to write out your own custom JSONSerialization
function for every app you create?
In the context of web-based REST API
s, they are how your app is going to talk to the world. There are hundreds of services out there that you might want to develop an app around
- A messaging app might use Firebase for realtime communication
- A social media app that aggregates your top tweets will make use of the Twitter API (Fabric)
- A cloud-based, file storage app may decide to use the Dropbox API for storing, retrieving and editing files
- Perhaps you have a new take on location-based B2B services, and will make use of the Foursquare API to load local business data
- Have a way to track pokemon in PokemonGo? Seems like a good place to use the Google Maps API to display live GPS data
Apps can use multiple REST
API's to perform complex tasks and create novel experiences for their users. With enough practice and skill, you can even develop your own APIs that other developers will use
Since there is a wide possibility of devices that use API's and many differnt APIs providing unique services, there needs to be a standard for how they can all communicate effectively. The most common format in use is JSON
(Javascript Object Notation). JSON
defines how the data returned from an API will be formatted, and at its core is really just a dictionary. You may even often hear json referred to as a "json dictionary." Though, the structure and content that json dictionary's key/value pairs is up to the API. This is what we mean when we say that "an API defines it's data response".
To get a sense of what JSON looks like, and how its used, let's go ahead and navigate to: http://maps.googleapis.com/maps/api/geocode/json
Not an exciting result, but there's way more here than you might initially expect. For one, we see that the page is essentially a dictionary with three keys, error_message, results, and status
. But why those three keys, and how can we know to expect them? And moreover, how did we know to go to that URL to even try this out?
As mentioned before, API's define a standard for how software can communicate. Meaning that they define the kinds of requests that can be made to them, along with the response they will return. JSON
defines the structure and syntax of the request/response, but its up to the API as to what should be included in a request and what could be included in the response.
All APIs will provide documentation on exactly what they expect in a request, and what they will return in a response.
If we check out https://developers.google.com/maps/documentation/geocoding/intro we'll be able to see every possible request and response that can be made by the API.
So looking at our previous request:
http://maps.googleapis.com/maps/api/geocode/json
... we see we're getting back an error. And judging by the APIs documentation, this is expected. Fortunately in the event of a bad request to an API, it is possible that the API returns a response with helpful error messages regarding what was bad about the request. In this case, we made an "invalid request" because we didn't include some additional information it was expecting to get along with the request. This additional information passed along with a request is called parameters. Parameters specify additional constraints on the requests we make. They may limit the data requested by filtering it by some criteria.
Example 1: Adding An Address to the API call
Go back to your prior request and add the "address" key along with a "value" of the address of C4Q For example: http://maps.googleapis.com/maps/api/geocode/json?address=%2243-06%2045th%20Street%22Note: Parameters appear following a single ? and spaces are replaced by %20 for the actual request, but the google api will understand the request the exact same way as if you put in "43-06 45th Street"
Example 2: Compare valid request with invalid one
1. Notice how we get new data passed along with a valid request? 2. Bring up the API documentation page again and compare the keys with what the API states will be returned 3. All of these results are listed in the API's documentation for this kind of response, so we will always know what to expectPlugging away in a web browser is a lightweight way to test out an API, but we can get some real power by using some utilities specifically meant for making API requests.
Try this out:
- Open Postman and enter the prior googleAPI request (http://maps.googleapis.com/maps/api/geocode/json)
- Add in "address" as a parameter along with some value
- If this worked, try some other addresses
{
"results": [
{
"address_components": [
{
"long_name": "The Falchi Building",
"short_name": "The Falchi Building",
"types": [
"premise"
]
},
{
"long_name": "31-00",
"short_name": "31-00",
"types": [
"street_number"
]
},
// ... much more json...
]
}
]
}
Testing API Requests and myJson
Think back to the URLSession
demo: I was calling our hosted json
an "API endpoint". And in some ways it does meet the requirement: we could send a request to the URL
and we would receive a formatted json
response. And at its core, an API is just a way to request and receive info based on a set of parameters. Though, in practice API's are a bit more detailed than simply plugging in a URL
endpoint and parsing out any data that gets returned. And it's likely that you'll be working with API's with incredible frequency in your development careers.
Making test requests is a big part of software, and iOS, development. So much so that a fellow developer created the myjson site after he became annoyed that there were no simple ways of hosting json with the intention of testing network requests and json parsing. Making web requests, specifically to service APIs, and parsing the returned response is going to be a part of a vast majority of iOS apps. Think about all the apps you use that you can log in with Facebook! That's it's own API that needs integration in an iOS app.
Fortunately, since working with API's is such a ubiquoitous thing for all developers (especially ios, android and web) there are quite a few resources and tools available to make development as smooth as possible. Some of the most important tools are those that let us test out API requests to verify that our inputs and outputs are correct and what we expect.
The Random User API
One of my favorite APIs to use for quick testing of creating user accounts is the Random User API. Many of the APIs discussed prior require some sort of authentication method, but fortunately the Random User API doesn't require it making it quite easy to hit the ground rounning with testing with it.
Side note, I found an easter egg in the source code of the site's documentation page and tweeted at the developer with my own take on it ๐ (it was a function named like konamiCode()). Could be a nice little anecdote on the development community culture in general. - Louis
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>nice easter egg @solewolf1993 .. needs more iddqd though. is it open for api requests? http://t.co/7djehsLWx4
โ Louis Tur (@louistur) April 14, 2015
Today is the beginning of the rest of everyone's social media account lives: we're going to start creating a brand new social platform called, iFacesterGram. We've lined up about $250,000 in seed money from an anonymous angel investor who attended our idea pitch event last week. Now it's time to get our little product off the ground. Our PM team wants us first to showcase what a list of users in the app would look like -- a sort of iFacesterGram "friends list." To complete this first assignment, we're going to need to accomplish a few things:
- Set up a basic UI to display our list of users
- Design a way to perform network calls to an API backend to get user data
- We're going to have to use some mock data for now, but we want robust sets of data
- Create our
User
model and decide how to parse data - Populate our list of users
- Implement a pull-to-refresh feature
Q1: What UI element should we use to display the list of users?
UITableviewController (Possibly) A custom UITableviewCellQ2: Based on what we know, what architetural design could we use for a network manager?
SingletonQ3: What class in the Swift Library can we use to parse raw data from URLRequests?
JSONSerializationQ4: What will we need to do to ensure that our list of users updates as soon as our network request finishes?
Wrapping up UI updating in a DispatchQueue.main.async blockWe begin this project with a few things already set up for us (read through and get familiar with the project):
- Three files already added:
User.swift
:struct
for our modelUsersTableViewController
: our primary view controller to displayUsers
APIRequestManager
: our singleton to manage network requests
- Storyboard has the following:
- An instance of
UsersTableViewController
with an embeddedUINavigationController
, set as the initial view controller - A single prototype
UITableViewCell
with the identifierUserTableViewCellIdentifier
Q1: What does private static let
indicate about our UserTableViewCellIdentifier
?
- Private indicates that the constant is only available to be used inside of the UsersTableViewController class
- Static indicates that this constant belongs to the UsersTableViewController class and not any 1 instance
- let indicates that the value does not change
Let's take a look at the data that would could potentially be working with. In Postman, set your URL to the random user API endpoint (https://randomuser.me/api/
) and hit "send" to make a request. Look at the json
that is returned.. what should we use to populate our user data?
What data should our social media app keep on a user for display?
There really is no wrong answer to thing, and it all depends on the design we'd like for our app. We're going to start off using first, last, city, state, username, email, id, and thumbnailNow, let's fill out the User
model using these properties
internal struct User {
internal let firstName: String
internal let lastName: String
internal let city: String
internal let state: String
internal let username: String
internal let emailAddress: String
internal let id: String
internal let thumbnailURL: String
}
For right now, our request manager just needs to be able to do a few things:
- Send a request to the random user API endpoint
- Do some basic error checking
- Use a callback closure to handle the returned
Data
Using what you know, and the previous lessons, design a singleton-based manager class with the function func getRandomUserData(completion: ((Data?)->Void))
. One you have it set up, call the function inside of the viewDidLoad
of UsersTableViewController
and make sure you're getting data and/or handling an error by printing its details out to console.
APIRequestManager.manager.getRandomUserData { (data) in
if data != nil {
print("Data returned!")
}
}
Q1: Our function definition is missing one thing, what is it?
It needs the@escaping
key word for the callback closure
Design Hints
A singleton needs two things: a class-level unchanging constantmanager
, and a private default initializer.
Its quite helpful to define unchanging properties (such as the URL for the api) as a static let
The more you know! Notice how there is a ton of useless messages being printed to your console? That's an annoying new addition to the latest version of Xcode. To silence this noise, hold down Option while clicking on the Run button (with the play button icon) in Xcode to bring up the "Scheme Manager". Under "Environment Variables", add a new entry with the name
OS_ACTIVITY_MODE
with a value ofdisable
. Now click "Run" and see a much cleaner console output!
Structs are nice in Swift because they give you a "free" initializer based on its properties (as long as you don't write your own). In this instance, we're going to use our free initializer, but we're going to be a bit more architecture-focused in how we do it. We're going to use a static func
on User
that takes in Data
and returns [User]?
. This static func
is essentially going to be like our InstaCatFactory
.
Guidelines:
- The function signature looks like
static func users(from data: Data) -> [User]?
- Make as many smaller functions as you'd like in order to convert from
Data
into[User]
(you can also use none, if you'd like, and just put all of the code inside ofusers(from:)
) - Test this thoroughly by running the app multiple times
In viewDidLoad
update the call to be:
APIRequestManager.manager.getRandomUserData { (data) in
if data != nil {
if let users = User.users(from: data!) {
print("We've got users! \(users)")
}
}
}
- Ignore the
let id: String
constant inUser
for now (this value is occasionallynil
)
- Alternatively, if you've done the normal casting and it all works, adjust the
id
parameter to be anOptional
- You'll run into an issue with casting from
"location"
if you attempt to cast it's contents to[String : String]
. The reason being that the value for thepostalCode
key is anInteger
. So the cast will fail if you try to set it as a[String: String]
. There are different approaches to how to handle this, but it might be easiest to get the value ofjsonDict["location"] as? [String : AnyObject]
, followed by getting the value for thestate
andcity
keys asString
As you parse out the Any
object into arrays and dictionaries, I would recommend adding print
statements along the way to see where something is working or failing
Set the cell's textLabel
to display a user's first and last name, and the detailLabel
to display their username
Implementation Hints
You'll have to updatenumberOfRows, numberOfSections, and cellForRow
You may want to add a variable to the tableview controller internal var users: [User] = []
Don't forget to update your UI properly! There's a special closure to bring stuff over from the "other" road into the "main" road
Re-running the project to get different data sets is kind of time consuming. It would be much better if we could just do the standard pull-to-refresh action wouldn't it? Heck yea.
Select the UserTableViewController
in storyboard and open it's Attribute Inspector
. Change the Refreshing
value from Disabled
to Enabled
In UserTableViewController
create a new function called func loadUsers()
and add in all of our APIRequestManager
code to it. Then in viewDidLoad
add self.loadUsers()
Create a new function called func refreshRequested(_ sender: UIRefreshControl)
and have it call our loadUsers()
function
Back in viewDidLoad
, add self.refreshControl?.addTarget(self, action: #selector(refreshRequested(_:)), for: .valueChanged)
Now run the project and try it out. Not perfect, but one step closer.
Create three more functions for our APIRequestManager
:
func getRandom(users: Int, completion: ((Data?)->Void) )
func getRandom(users: Int, gender: UserGender, completion: ((Data?)->Void) )
func getRandom(users: Int, nationality: UserNationality, completion: ((Data?)->Void) )
For each of these, the users
parameter will be an Int
that will change the number of results returned on an API call. UserGender
and UserNationality
should be two enum
that correspond to the possible options as listed in the RandomUserAPI documentation (including a "no-preference" option).
Code Hints
- Your enums will look like this:
enum UserGender: String { ... }
andenum UserNationality: String { ... }
- Constructing URL's is more easily done by starting off with a
String
and then later usingURL(string:)
when the string is ready. - Pay attention to the documentation on making requests with parameter.
Advanced
Using default parameter values, find a way to condense all of the four functions we now have in APIRequestManager
into just one.
- Why You should love default parameter values - Natasha the Robot
- Parameter Defaults and Optional Function Parameters
Expert
Create a separate "factory" class to generate the appropriate URL
given the different possible parameters of users
, nationality
and gender
. Call this factory RandomUserURLFactory
with a singleton called manager
. This factory class is intended to be used by the APIRequestManager
.
RandomUserURLFactory
will have one function: func endpoint(users: Int, nationality: [UserNationality], gender: UserGender) -> URL
. From those parameters, build an appropriate URL to make a request. There are no particular rules on how you should go about doing this, so feel free to explore code. Just be sure to use Postman to test URLs and make sure you have an understanding of the format of the parameters.
Examples:
Input | RandomUserURLFactory.manager.endpoint(users: 10, nationality: [.GB], gender: .male) |
Output | https://randomuser.me/api/?results=10&nat=GB&gender=male |
Input | RandomUserURLFactory.manager.endpoint(users: 2, nationality: [.AU,.BR,.GB], gender: .noPreference) |
Output | https://randomuser.me/api/?results=2&nat=AU,BR,GB&gender= |
Part 1: Returning an error:
Printing out that an error has occurred is helpful and all, but it's not exactly the best way to handle errors. The point of errors is that you can detect them and then deal with them "gracefully". With that in mind, we're going to have our users(from:)
function potentially return an Error
. Change the function to static func users(from data: Data) -> ([User]?, UserError?)
and update the code so that each one of the guard else
statement returns a different error corresponding to the casting that failed.
Note: UserError
will be an enum that conforms to Error
and has multiple cases for each of the potential errors.
Advanced
Returning a tuple is fine, but let's instead define a new tuple called UserParseResults
that is of type ([User]?, UserError?)
Expert
Instead of errors returned, have your users(from:)
throw
a UserError
. Update your function calls in viewDidLoad
to account for this change.