A student that completes this project shows that they can:
- understand and explain the difference between PUT and POST
- use URLSession to send a PUT request to create or modify data on a REST endpoint
- use URLSession to send a POST request to create data on a REST endpoint
Message Board is an application that will help you solidify the differences between PUT and POST HTTP methods. It allows users to create a message thread, and add messages to the threads.
Please look at the screen recording below to know what the finished project should look like:
Please fork and clone this repository. This repository does not have a starter project, so create one inside of the cloned repository folder.
For reasons that will be discussed later in these instructions, we're going to start the project in the storyboard.
- Delete the view controller that comes with the Main.storyboard.
- Add a
UITableViewController
scene, and embed it in a navigation controller. - Set the navigation controller as the initial view controller.
- Add a
UIView
as the header view to the table view controller, then add aUITextField
to the header view. - Give the navigation item a title of "λ Message Board".
- Give the cell an identifier, and set its style to "Basic". Give it an identifier as well.
- Create a Cocoa Touch Subclass of
UITableViewController
calledMessageThreadsTableViewController
and set this scene's class to it. - Add an outlet from the text field, and an action from it as well. Set the action's event to "Did End On Exit".
- Add a second
UITableViewController
scene to the storyboard. - Create a "Show" segue from the first table view controller's cell to this new table view controller scene. Give the segue an identifier.
- Set the table view's cell's style to "Subtitle". This cell will display a
MessageThread.Message
object's properties. - Add a navigation item to the table view controller scene. Then add a bar button item in the top right of the navigation bar. Set it's "System Item" to "Add".
- Create a Cocoa Touch Subclass of
UITableViewController
calledMessageThreadDetailTableViewController
and set this scene's class to it.
- Add a
UIViewController
scene to the storyboard. - Create a "Show" segue from the
MessageThreadDetailTableViewController
's bar button item to this newly added view controller scene. Give the segue an identifier. - Add a
UITextField
to the scene. Give it a placeholder of "Enter your name:". - Add a
UITextView
to the scene. - Constrain the text field and text view.
- Add a navigation item to the scene.
- Add a bar button item to the top right of the navigation bar. Set its title to "Send".
- Create a Cocoa Touch Subclass of
UIViewController
calledMessageDetailViewController
and set this scene's class to it. - Create outlets from the text field and text view, and an action from the "Send" bar button item.
We're going to set up the functionality to PUT and POST our model objects right now, then come back to fetching them later.
This application will use two model objects. These model objects will form a parent-child relationship between each other. This is a common practice when dealing with JSON from a REST API. The idea behind the relationship in the context of REST APIs is that you make a model object for each "layer" of JSON. This allows you to pick out the properties you care about using in your app individually at every level. It requires a bit more of set up than you may be used to, but it makes using the data that you get from the API in the rest of your application normal.
This is the JSON object we are going to set up and use:
Or you can use this with a JSON viewer if you want (they're the exact same):
{
"695398C4-498C-40A8-AA76-CB2B20DFD9FA": {
"identifier": "695398C4-498C-40A8-AA76-CB2B20DFD9FA",
"messages": {
"-LJNQ2zNeL4bwG1qpwYB": {
"sender": "Spencer",
"text": "This is a test to make sure the API works!",
"timestamp": 5.55405872056162E8
}
},
"title": "REST APIs"
}
}
- Create a "MessageThread.swift" file, and create a
MessageThread
class. There is a reason for making this a class which will be explained at a point where you can see the difference it will make later on in these instructions. Adopt theCodable
protocol.- NOTE: We are calling the class
MessageThread
because there is already a class calledThread
. As you go along in this project, make sure you don't get the two confused and accidentally use theThread
class instead.
- NOTE: We are calling the class
- Create the following properties:
- A
title
string constant. - An
identifier
string constant.
- A
- We need to create a separate model object for the messages within the thread. This goes back to the concept of making a model object for each "layer" of the JSON. You may notice that the value for the
"messages"
key in the JSON is another dictionary. Inside of theMessageThread
class, create a struct calledMessage
.
seem a bit odd to nest a class inside of a class or a struct inside of a struct, but this is fairly common when using Codable
. However, this may change as time goes on. Codable
is relatively new as it was only released in Swift 4. In order to refer to this Message
class, you must write MessageThread.Message
.
- In the
Message
struct, add the following:- A
text
string constant. - A
sender
string constant. - A
timestamp
Date
constant. - An initializer that gives a default value of the current time to the
timestamp
property. HINT: When calling the date initializer (Date()
), the current time is taken for its value. - Adopt the
Equatable
andCodable
protocols.
- A
- In the
MessageThread
class, add amessages: [MessageThread.Message]
variable. - Create an initializer for the
MessageThread
class. Give a default value of an empty array to themessages
property, and a value ofUUID().uuidString
to theidentifier
property. - Adopt the
Equatable
protocol in theMessageThread
class. You will need to manually implement the==
function. HINT: This article will help you get started it if you're unfamiliar with how to do it. Of course, feel free to ask your PMs for help as well.
At this point, the model objects are set up to be able to be saved to the API, but we'll have some trouble fetching them. There is one last thing to implement in order for them to be decoded correctly when fetching them from the API, but we'll implement it later.
- Create a "MessageThreadController.swift" file and a
MessageThreadController
class inside of it. - Add a
messageThreads: [MessageThread] = []
variable. - Add the following code snippet to the class:
static let baseURL = URL(string: "https://lambda-message-board.firebaseio.com/")!
. NOTE: In order to access thisbaseURL
, you must call the classMessageThreadController
, then.baseURL
because this is a static property.
You will now make the method to create and send a MessageThread
to the API. This method will use PUT as its HTTP method:
- Create a function called
createMessageThread
. It should take in atitle
parameter, and have a completion closure as well. The completion closure should use@escaping
. - Inside the function, initialize a new
MessageThread
object. - We need to create a
URL
using the thread'sidentifier
property. This will let us put the thread at a unique location in the API. To do this, use theappendingPathComponent
method on thebaseURL
property. (again, you will need to use theMessageThreadController
class to access thebaseURL
.) Since we are using Firebase as the API, you must also append a"json"
path extension (usingappendingPathExtension
) to the URL or the requests will not work at all. The full URL should look something like this:https://lambda-message-board.firebaseio.com/695398C4-498C-40A8-AA76-CB2B20DFD9FA/.json
. Note that it doesn't matter if the last/
is in the URL or not. - Create a
URLRequest
variable with theURL
you just created. Set itshttpMethod
to"PUT"
. - Encode the
MessageThread
object you just initialized usingJSONEncoder
in a do-try-catch block. - Perform a
URLSessionDataTask
with theURLRequest
you just created. Handle the potential error returned in the completion closure. If there was no error, then append theMessageThread
object to themessageThreads
variable. Remember to callcompletion
, and resume the data task.
You now need a method to create messages within a MessageThread
. In order to do that:
- Create a function called
createMessage
. It should take in aMessageThread
parameter (so you can put the new message in the correct thread),text
andsender
string parameters, and an escaping completion closure. - Inside of the function, initialize a
MessageThread.Message
object from the parameters in this function. - Create a
URL
just like you did in the previous function by using thebaseURL
and theMessageThread
parameter'sidentifier
. This time, also append another path component called"messages"
. NOTE: Because of the wayCodable
works by default, this string should match the name of theMessageThread
's array of messages property or you will run into trouble later on. After you've appended the identifier and "messages" to theURL
, append the"json"
path extension. This path extension must be at the end of theURL
. - Create a
URLRequest
variable with theURL
you just created. Set itshttpMethod
to"POST"
. - Encode the
MessageThread.Message
object you just initialized usingJSONEncoder
in a do-try-catch block. - Perform a
URLSessionDataTask
with theURLRequest
you just created. Handle the potential error returned in the completion closure. If there was no error, then append theMessageThread.Message
object to theMessageThread.Message
variable. Since theMessageThread
is a class, you can directly append it to its array ofmessages
here. Remember to callcompletion
, and resume the data task.
- In the
MessageThreadsTableViewController
, add amessageThreadController: MessageThreadController
property. Set its default value to a new instance ofMessageThreadController
. - Using the
messageThreadController
, implementnumberOfRowsInSection
andcellForRowAt
. Each cell should display the title of its correspondingMessageThread
object. - In the action of the text field, unwrap its
text
and call thecreateMessageThread
method, passing in the unwrapped text for the new thread's title. In the completion closure ofcreateMessageThread
, reload the table view on the main queue. - Go to the
MessageThreadDetailTableViewController
and create the following varables:messageThread: MessageThread?
messageThreadController: MessageThreadController?
- Back in the
MessageThreadsTableViewController
, implement theprepare(for segue: ...)
method, passing themessageThreadController
, and theMessageThread
that corresponds to the cell the user tapped on.
- In the
MessageThreadDetailTableViewController
and using themessageThread
variable, implement thenumberOfRowsInSection
, and thecellForRowAt
methods. The cell should display thetext
andsender
of its correspondingMessageThread.Message
object. - In the
viewDidLoad
, set the title of the view controller to thetitle
of themessageThread
object. - Add the
viewWillAppear
method. Inside it, simply reload the table view. - Go to the
MessageDetailViewController
and add the following variables:messageThread: MessageThread?
messageThreadController: MessageThreadController?
- Back in the
MessageThreadDetailTableViewController
, implement theprepare(for segue: ...)
method, passing this view controller'smessageThreadController
, and themessageThread
to theMessageDetailViewController
.
- In the bar button item's action in the
MessageDetailViewController
:- Unwrap the text from the text field and text view outlets as well as the
messageThread
property. - Call the
messageThreadController's
createMessage
method, passing in the newly unwrapped values. - In the completion of this function, pop the view controller on the main queue.
- Unwrap the text from the text field and text view outlets as well as the
At this point, you should be able to test the app to see if you are able to create threads and messages, but you won't be able to fetch them when the app loads quite yet.
Since the messages are being POSTed (thus creating a unique identifier key with the message as the value), we will run into an issue when trying to decode the fetched data from the API into MessageThread
objects. In order to fix this, we are going to manually tell the decoder how to decode MessageThread
s instead of letting Codable
try to do it for us.
- In the
MessageThread
class, add this initializer. Since we haven't covered this before now, we'll talk about each step marked with comments:
required init(from decoder: Decoder) throws {
// 1
let container = try decoder.container(keyedBy: CodingKeys.self)
// 2
let title = try container.decode(String.self, forKey: .title)
let identifier = try container.decode(String.self, forKey: .identifier)
let messagesDictionaries = try container.decodeIfPresent([String: Message].self, forKey: .messages)
// 3
let messages = messagesDictionaries?.compactMap({ $0.value }) ?? []
// 4
self.title = title
self.identifier = identifier
self.messages = messages
}
When this initializer is implemented, JSONDecoder
(or any Decoder
) will use the logic in this initializer to know how to parse the JSON and where to put the information in the MessageThread
object that is being initialized.
- Comment 1: The
container
is aKeyedDecodingContainer
object. This object allows us to access the key-value pairs of the JSON in the decoder. We give it theCodingKeys
enum of ourMessageThread
object so it knows which key-value pairs we want in the JSON. Notice that you never implemented aCodingKeys
enum in this class. If you don'tCodable
will synthesize one for you based on the names of the class or struct's properties. - Comment 2: Using the
container
object, we pull out the individual values from each key-value pair. We specify the type we expect the value to be such asString.self
or in the case ofmessageDictionaries
, it is[String: MessageThread.Message]
. We also use theCodingKeys
cases to specify which key-value pair to pull the value from in the JSON. - Comment 3: As stated previously, since you are POSTing the messages, the API will create a unique identifier as the key, and use the
MessageThread.Message
as the value. This is the part that would break the decoding if we didn't implement this initializer. In order to circumvent this problem, we take eachMessageThread.Message
object and sort of discard the identifier key by mapping through the dictionaries and returning only the value, which is the actual message object. We use thedeciodeIfPresent
method because if theMessageThread
doesn't have any messages in it (i.e. right when the message thread is created), it will throw an error if we usedecode
.decodeIfPresent
will try to decode it, but if there is no value, it will simply return nil instead. - Comment 4: We then set the classes properties to the newly decoded properties, similar to a normal memberwise initializer.
In the MessageThreadController
, create a function fetchMessageThreads
. This method should have an escaping completion closure. Inside of the function:
- Create a
URL
that takes thebaseURL
and appends the"json"
path extension onto it. Again, this is only necessary when using Firebase as the API. - Create a
URLSessionDataTask
using thedataTask(with: URL, ...)
convenience method. - Inside of the data task's completion closure, check for an error.
- Unwrap the data, and using a
JSONDecoder
object and a do-try-catch block, decode the JSON as[String: MessageThread].self
into a constant calledmessageThreadDictionaries
. If you are unclear as to why we decoding the JSON this way, refer back to the example JSON. At the highest level, theMessageThreads
are the values of UUID string keys. Make sure to handle errors in thecatch
statement. - Create a constant called
messageThreads
. For its value,map
themessageThreadDictionaries
and return only the values of each dictionary. - Set the class's
messageThreads
variable to themessageThreads
constant you just made so the rest of the application can use the threads. - Call
completion
.
Finally, in the MessageThreadsTableViewController
, call the viewWillAppear
method. Inside of it, call the messageThreadController
's fetchMessageThreads
method. In the completion closure, reload the table view on the main queue. This will allow the table view controller to fetch any new threads and messages made every time this table view controller appears.
- Add a
UIRefreshControl
to theMessageThreadsTableViewController
that will allow the user to "Pull to refresh" the table view. This should re-fetch the message threads from the API. - Using the
timestamp
property on theMessage
object, sort the messages chronologically. Sort the list ofMessageThreads
alphabetically.