REST Exercise
In this exercise we’ll hook up our login screen from the previous exercise with a real API.
The main goal of this exercise is for you to learn how to query REST APIs using URLSession.
Deadline
See the deadline on the exercises page.
Submission instructions
See the submission instructions on the exercises page.
API Key, Username, Password
In this exercise, we’re going to use the REST API of the Google Firebase service. Specifically, we’re going to use this endpoint to sign in with an email/password combo. You shouldn’t necessarily have to dig through the docs above to be able to complete this exercise. Everything you need should be in the instructions below, but the above link might still help you understand the inner workings of the API.
When using the API, use the following credentials:
- API Key:
AIzaSyCTryhlVmmRHYE7iQT3k0eeNRHIKsTMpRw - Email:
m@m.at - Password:
madmad
Note that I intentionally chose a short email/password combo to make testing easier for you so you don’t have to type so much. Since this user account doesn’t have access to any important data, security isn’t really a concern.
Instructions, Requirements and Hints
Setting up the project
- Continue with the Xcode project from the previous exercise
Setting up the login request
- Create a new Swift file. In this file, create a new class that will handle all the networking
- In your new networking class create a constant
URLSessionand initialize it with the default configuration - Add a
loginmethod to your networking class. It should take two parameters: email, password. Later in this exercise, we’ll add a third parameter, acompletionHandlerclosure, but this is enough to get started. - Create a
URLwith the following endpoint:"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=API_KEY" - Create a
URLRequestwith the above URL. - Set the
URLRequest'shttpMethodto"POST". - Set the
URLRequest'shttp header forContent-Typetoapplication/json. - Set the request body. It should be a JSON string in the following format:
{
"email": "YOUR_EMAIL"
"password": "YOUR_PASSWORD",
"returnSecureToken": true
}
- To get an appropriately formatted JSON string, you need to construct a dictionary of type
[String: Any], which contains the above keys and data. Then, useJSONSerialization.data(withJSONObject: dictionary, options: [])to serialize the data. Since this method maythrowan error, for now you can usetry!to disable error propagation. Later in this exercise we’ll properly handle the error. - Now use the
URLSessionconstant you created earlier to create aURLSessionDataTaskwith theURLRequestyou created. Take a look at this method if you’re stuck. - In the above method’s
completionHandler, print out the three parameters: theData,URLResponseandError. You’ll have to convert theDataobject into aStringfirst. You can do so by callingString(data: data, encoding: .utf8). - Call the
resume()method on your newly createdURLSessionDataTaskto start the request. - Find a good place to create an instance of your networking class and call your new
loginmethod. For example, you could call the method from the action that is called when the user presses the login button in theViewController. For testing, you may want to temporarily hardcode your email and password so you don’t have to manually re-type it every time you restart the application. - Take a look at the console output. If you get a response in the following format, you were successful!
{
"kind": "identitytoolkit#VerifyPasswordResponse",
"localId": "CxlvybnrRnSZyDDXN2VS7Fr6ddk1",
"email": "YOUR_EMAIL",
"displayName": "",
"idToken": "VERY_LONG_ID_TOKEN",
"registered": true,
"refreshToken": "REFRESH_TOKEN",
"expiresIn": "3600"
}
Properly parsing the response
- Now that we know our request is working, we want to parse the response properly.
- Create a new Swift file called
User.swift. In this file, create a newUserstruct. - Add properties to your
Usertype that correspond to the response from the API. Make sure to includelocalId,email,displayName,idToken,registered, andrefreshToken. Make sure the variables are appropriately typed. - Make your user type conform to the
Codableprotocol. If you’ve named your properties exactly as they’re named in the API response, you shouldn’t have to do anything other than adding the protocol to the type’s definition. Read this article if you’re stuck. - In the
URLSessionDataTask'scompletionHandler, use aJSONDecodertodecodethe data, using your newUsertype. For now, it’s okay to disable error propagation usingtry!. - Print out your newly created
Userobject to see if everything’s working. - If you’re having trouble with this part of the exercise, you can refer to this tutorial.
Handling errors
- We now can log in and properly parse a returned
Userobject. However, so far we haven’t handled any of the errors. We should fix that! - Note, this is arguably the hardest part of this exercise, or at least the most tedious/time consuming.
- We’ll ultimately want to provide information to the user what went wrong. To do so, we want to create a new
enumtype, calledNetworkingError. This new type should conform to the SwiftErrorprotocol. - Make sure the following error cases are handled individually. Add enough enum
casesso that they cover all the error cases in the following list.- An error when serializing the dictionary containing the email and password
- User entered something that isn’t an email address
- User entered the wrong email address
- User entered the correct email address, but the wrong password
- The network is offline
- The http response contains data in an unexpected format
- The http response has an non-successful response code (for example due to an internal server error)
- Unexpected errors that we didn’t think of yet
- For example, try what happens if you enter an invalid email address. The application will probably crash, since we’re still using
try!to parse the user response. This is because thedatain thecompletionHandlerwill now contain JSON in the following format:
{
"error": {
"code": 400,
"message": "INVALID_EMAIL",
"errors": [
{
"message": "INVALID_EMAIL",
"domain": "global",
"reason": "invalid"
}
]
}
}
- To parse this error, create another
structtype, (maybe name itResponseError) that conforms toCodable. This is a bit more difficult to parse than ourUserfrom above, since it contains nested dictionaries and an array. You’re going to have to use customCodingKeysas described in this article. You may even have to create another type that conforms toCodableto parse the nested errors. If you’re having difficulties, look at this StackOverflow post for hints. - Use
printto look at thedata,responseanderrorin thecompletionHandler. Try some of the other error cases from above to see what happens. - A hint: if the
errorreturned in thecompletionHandlerclosure is notnil(for example in the case of no internet connection),errorwill actually contain anNSErrorinstance. You can use a conditional downcastif let error = error as NSError?to cast it to anNSError. You can then useerrror.localizedDescriptionto obtain a nicely formattedStringthat you can later present to the user. You can then use enum associated values to associate this data with your enum cases. - Make sure that each of the above error cases is handled. For each case, create an appropriate instance of your
NetworkingErrorenum. If each case is handled, you’re done with this part of the exercise.
Creating a completion handler and forwarding the user/error
- We can now successfully parse the
Userand handle error cases by creating appropriateNetworkingErrorenum cases. We now want to forward this data asynchronously to the caller of theloginmethod. - To do this, add a
completionHandlerclosure to theloginmethod parameters. The parameters of thecompletionHandlershould be an optionalUser?object and an optionalNetworkingError?object. Similar to how thecompletionHandlerfor theURLSessionDataTaskis implemented. If you’re having trouble with the syntax, try CMD-clicking thedataTask(with request...method you implemented earlier to jump to its definition. This should give you an idea how it’s defined. - You have to mark the
completionHandlerclosure as@escapingin its definition. If you don’t remember the difference between escaping and non-escaping closures, you can read up again about the difference here. - Now that we have defined the
completionHandler, it’s time to call it from ourloginimplementation! - Make sure to call the
completionHandlerwith theNetworkingErroryou created as its parameter from all the error cases described above. ThecompletionHandlershould only ever be called once in the entireloginmethod. - Since most of the error cases will happen in the
URSessionDataTaskcompletionHandler, we’re going to run into a problem. Remember that thedataTaskis running asynchronously. This means thedataTask'scompletion handler is not called on the main thread, but on a background thread! However, we’re not allowed to update the UI from any other thread than the main thread. Since the caller of ourloginmethod will most likely want to update the UI, we should make sure that ourcompletionHandleris instead called from the main thread. You can use the following code snippet to do so:
DispatchQueue.main.async {
completionHandler(user, nil)
}
- You should now update the place where you call the
loginfunction to include thecompletionHandler. There, you can check if theUser?orNetworkingError?is present. - In case of an error, display an appropriate error message, describing the error, using a
UIAlertControllerlike in the login exercise. You can use aswitchstatement on theNetworkingErrorenum for this to create the error messages. - In case of success, simply display a success message. We’ll further expand upon this in a future exercise.
Help and Support
As always, if you need any help or have any questions, feel free to contact me. I’m happy to help!