URLSession
- Apple (just the "Overview section for now")- Move from NSURLConnection to Session - Objc.io
- Fundamentals of Callbacks for Swift Developers
- Concurrency - Objc.io
- Concurrency’s relation to Async Network requests (scroll down)
- Singletons in Swift - Thomas Hanning
- Singletons - That Thing in Swift
- The Right Way to Write a Singleton - krakendev (gives a nice short history of writing singletons in objc and swift. then shows you how/why they are truly "single". This blog in general is a really good resource worth bookmarking)
- Great talk on networking - Objc.io (a bit advanced! uses flatmap, generics, computed properties and closure as properties)
- Concurrency - Wiki
- Understanding that
URL
truly is "universal" as it can refer to a file on your local machine, or a web URL - Handling
json
data from web requests is exactly like dealing with a localdictionary/json
- Reusuability of code makes building something up to the point much easier as we can reuse the same parsing function from the prior lesson here.
In MVC a (view) controller is only meant to coordinate data from the model and use that to update its views. In the first part of this lesson (AC3.2-NSURL), we put all of our code inside of our main view controller class. This lesson begins with that same base code, but we're going to refactor it so that we follow MVC principles.
Instructions: Follow the steps below in order to refactor your code. Make sure that your tests still pass after you've made your changes.
- Create a separate
InstaCat.swift
file, and move the code for theInstaCat
model there - Create a
- Create another file
InstaCatParser.swift
which will be tasked with taking aURL
and parsing out[InstaCat]
- Add the following template and fill out your functions using the code you wrote for
NSURL
class InstaCatParser {
init(){}
// get an URL from String
func getResourceURL(from fileName: String) -> URL? {
return nil
}
// get Data from file located at URL
func getData(from url: URL) -> Data? {
return nil
}
// parse Data into InstaCats
func parseInstaCats(from data: Data) -> [InstaCat]? {
return nil
}
}
- The point of the
InstaCatParser
is that it is going to handle all parsing ofInstaCats
. Create a single function calledfunc parseCats(from: String) -> [InstaCat]?
that calls the other three functions at once. This will be a convenience function to get us from aString
representation of a file URL all the way to our desired[InstaCat]
- Can you think of a way to ensure that only
parseCats(from:)
can ever be called? Why would this be advantageous to use? - Rewrite the code inside of
InstaCatTableViewController.viewDidLoad
to make use ofparseCats(from:)
- Adjust the
InstaCatParser
to use a singleton. Future calls to the parser should then look likeInstaCatParser.shared.<function>
- Update your code in
InstaCatTableViewController.viewDidLoad
and elsewhere to work with this singleton.
The goal of a singleton is that there only is ever one of them that exists in the lifetime of your app. Singletons work well for managing data in simple apps, but can cause problems in more complex situations.
Start out by adding the following line to the top of your InstaCatTableViewController
:
let instaCatEndpoint: String = "https://api.myjson.com/bins/254uw"
(Plug in the URL into your browser too, just to see what comes up)
Note: If the above URL isn't working, use:
https://raw.githubusercontent.com/C4Q/AC3.2-NSURLSession/master/Resources/JSON/instacat.json
A fundamental principal in computer systems architecture is that the "internet" is just a lot of interconnected computers. Every image you've ever viewed, every video you've ever watched, and every website you've visited are files that live on someone else's computer, that you were accessing with your own.
That's why when you plug in that previous URL into a browser, you're seeing exactly what you would see in a file called 254uw.json
if it lived on your computer. A URL describes where a file is, whether that's on your computer or on someone else's on the internet. In this case, there is a service called myjson that hosts json files that you create. The one we're looking at is one that I made using the exact same data/file as InstaCat.json
In fact, if you located the
InstaCat.json
file that lives on your computer and dragged it into your browser window, the address bar would change to the URL of the file's local address, and you'd see the same data. The url would look something likefile:///Users/<your_user>/<path>/<to>/AC3.2-NSURLSession/Resources/JSON/instacat.json
. Notice how it saysfile://
instead ofhttp://
!
What actually happens when you entered that myJson URL
into your browser?
Your browser sends what's known as a GET
request to https://api.myjson.com
. A GET
request indicates that you're looking to get something from the URL
you're requesting. The GET
request we send contains more specific information: because we're interested in getting a specific set of json, we pass in two more path components: /bins
and /254uw
. If the request can be fulfilled by the receiving website, a response is returned corresponding to either the data we were looking for or an error describing what went wrong with the request.
Let's take a look at how we can make a request to api.myjson.com
:
To our InstaCatTableViewController
, add
func getInstaCats(from apiEndpoint: String) -> [InstaCat]? {
if let validInstaCatEndpoint: URL = URL(string: apiEndpoint) {
}
}
Every web request begins with a URLSession
, which gets instantiated with a specified URLSessionConfiguration
. For our purposes, we will only ever use URLSessionConfiguration.default
.
Note: Connecting to sites in order to send or receive data is known broadly as a session, which is why the class that manages making requests and receiving responses is called
URLSession
.
func getInstaCats(from apiEndpoint: String) -> [InstaCat]? {
if let validInstaCatEndpoint: URL = URL(string: apiEndpoint) {
// 1. URLSession/Configuration
let session = URLSession(configuration: URLSessionConfiguration.default)
}
}
We then use the dataTask(with:)
method of URLSession
to initiate our request
func getInstaCats(from apiEndpoint: String) -> [InstaCat]? {
if let validInstaCatEndpoint: URL = URL(string: apiEndpoint) {
// 1. URLSession/Configuration
let session = URLSession(configuration: URLSessionConfiguration.default)
// 2. dataTaskWithURL
session.dataTask(with: validInstaCatEndpoint) { (data: Data?, response: URLResponse?, error: Error?) in
}
}
}
Why all the optionals in
dataTask(with:)
? Well, you might not getData
back if the request is bad, you might not get a response if the internet is down, or you might not get anError
is everything goes right.
Best practices says to check for errors first
func getInstaCats(from apiEndpoint: String) -> [InstaCat]? {
if let validInstaCatEndpoint: URL = URL(string: apiEndpoint) {
// 1. URLSession/Configuration
let session = URLSession(configuration: URLSessionConfiguration.default)
// 2. dataTaskWithURL
session.dataTask(with: validInstaCatEndpoint) { (data: Data?, response: URLResponse?, error: Error?) in
// 3. check for errors right away
if error != nil {
print("Error encountered!: \(error!)")
}
}
}
}
At this point, we could check to see if we have any data being returned and print it out...
func getInstaCats(from apiEndpoint: String) -> [InstaCat]? {
if let validInstaCatEndpoint: URL = URL(string: apiEndpoint) {
// 1. URLSession/Configuration
let session = URLSession(configuration: URLSessionConfiguration.default)
// 2. dataTaskWithURL
session.dataTask(with: validInstaCatEndpoint) { (data: Data?, response: URLResponse?, error: Error?) in
// 3. check for errors right away
if error != nil {
print("Error encountered!: \(error!)")
}
// 4. printing out the data
if let validData: Data = data {
print(validData) // not of much use other than to tell us that data does exist
}
}
// 4a. ALSO THIS!
}.resume()
}
You're almost always going to forget to add .resume()
at first and until you call it, the dataTask
won't actually execute.
Anyhow that doesn't give us much information as the data is still in its raw encoded form. Now, in theory the Data
we're getting back is exactly the same at if we were accessing the data from InstaCat.json
. soo...
func getInstaCats(from apiEndpoint: String) -> [InstaCat]? {
if let validInstaCatEndpoint: URL = URL(string: apiEndpoint) {
// 1. URLSession/Configuration
let session = URLSession(configuration: URLSessionConfiguration.default)
// 2. dataTaskWithURL
session.dataTask(with: validInstaCatEndpoint) { (data: Data?, response: URLResponse?, error: Error?) in
// 3. check for errors right away
if error != nil {
print("Error encountered!: \(error!)")
}
// 4. printing out the data
if let validData: Data = data {
print(validData)
// 5. reuse our code to make some cats from Data
let allTheCats: [InstaCat]? = InstaCatParser().parseInstaCats(from: validData)
print("All the cats!! \(String(describing: allTheCats))")
}
}
// 4a. ALSO THIS!
}.resume
}
Go ahead and call getInstaCats(from:)
from viewDidLoad
and make sure it prints out some data!
let instaCatEndpoint: String = "https://api.myjson.com/bins/254uw"
override func viewDidLoad() {
super.viewDidLoad()
// if let instaCatsAll: [InstaCat] = InstaCatParser().parseCats(from: instaCatJSONFileName) {
// self.instaCats = instaCatsAll
// }
_ = self.getInstaCats(from: instaCatEndpoint)
}
Ok, let's now have our code return the [InstaCats] we've created just as we did in the (now) commented out code: let instaCatsAll: [InstaCat] = InstaCatParser().parseCats(from: instaCatJSONFileName)
. Update getInstaCats(from:)
to return allTheCats
...
error:
Unexpected non-void return value in void function
We can't return
from this block because dataTask(with:completionHandler:)
has a function type of (URL, (Data?, Response, Error)->()) -> ()
, so it isn't expected to return anything.
We're going to need to make some changes to the function to have it function correctly. The simplest is to remove the return
value and just do our UI updating work in the tableview controller directly.
func getInstaCats(from apiEndpoint: String) { // implicitly returns Void
if let validInstaCatEndpoint: URL = URL(string: apiEndpoint) {
// 1. URLSession/Configuration
let session = URLSession(configuration: URLSessionConfiguration.default)
// 2. dataTaskWithURL
session.dataTask(with: validInstaCatEndpoint) { (data: Data?, response: URLResponse?, error: Error?) in
// 3. check for errors right away
if error != nil {
print("Error encountered!: \(error!)")
}
// 4. printing out the data
if let validData: Data = data {
print(validData)
// 5. reuse our code to make some cats from Data
// let allTheCats: [InstaCat]? = InstaCatParser().parseInstaCats(from: validData)
print("All the cats!! \(String(describing: allTheCats))")
// 6. if we're able to get non-nil [InstaCat], set our variable and reload the data
if let allTheCats: [InstaCat] = InstaCatParser().parseInstaCats(from: validData) {
self.instaCats = allTheCats
self.tableView.reloadData()
}
}
}.resume() // Other: Easily forgotten, but we need to call resume to actually launch the task
}
}
And lastly, add this to viewDidLoad
:
self.getInstaCats(from: instaCatEndpoint)
Rerun the project... Awesome! The data printed out to console.. but nothing showed up in our table view? You just met one of the biggest issues you'll encounter with network calls: updating your UI when your data is ready. Here's the most common way to update your UI following a network request:
// update the UI by wrapping the UI-updating code inside of a DispatchQueue closure
DispatchQueue.main.async {
self.tableView.reloadData()
}
If you aren't familiar with DispatchQueue
yet, just know that you need to wrap up your UI-updating code in DispatchQueue.main.async
All should be well now: your request is made, data is retrieved and your tableview's UI reloads. But you might be left with some questions:
- Why didn't we follow the MVC design pattern and put this calling code in
InstaCatParser
? - If we do move this into
InstaCatParser
, how do we get the[InstaCat]
array to the table view controller if we can't return a value fromgetInstaCats(from:)
- Why do we need to update the UI in such a special manner?
Concurrency in computer science refers to order independent execution and handling. More importantly, it also says that although the actions you take are out of order, you always get the same expected results at the end.
For example, if you need to cook a pasta dish you could follow these broard instructions:
- Add water to a pot, turn on the heat.
- Wait
- When it starts boiling, add the pasta
- Wait
- Strain the pasta when it is done cooking
But doing things one after another like this, referred to as serial execution, would take a very long time, despite the guarantee that everything necessary will be done by a certain time. Not only would it take a long time, there would be stretches of time where you were doing nothing... just standing there, staring at a pot of water. A much more efficient way of doing is through doing concurrent operations:
- Add water to pot, turn on heat
- While water comes to a boil, chop ingredients for sauce
- Add pasta to boiling water
- While the the pasta cooks, add sauce ingredients to a pan
And if you have multiple burners on your stove, then it makes even more sense to do things in a concurrent manner.
Well, computers kind of work the same way. It used to be that computers only had one "burner" (called a "single core" processor) available but now they have many more at their disposal ("multi-core" processors). The purpose of this is to do as many things as possible at the same time and allow the computer to bounce between tasks as needed.
Networks can be unreliable, especially on a mobile device. And even when the network connection is good there's still a non-trivial amount of time needed for content, especially images and video, to load. Though, while that loading takes place your phone is still working, doing hundreds of other things. Though when the image or video finally does load, it appears on screen and you continue your browsing. Thanks to (in part) concurrency, your phone continues to add things to its burners while one of them waits for the "water" to boil.
More specifically, when we talk about concurrency in networking requests, it’s usually in the realm of asynchronous requests. An asynchronous request is similar to concurrency in that you start it, go do something else for a little bit, and then you get alerted when it is finished. In a kitchen, you may ask your sous chef to go prep some onions for your pasta sauce while you do something else. Then once they finish chopping, you get the chopped onions back. You don’t know exactly how long this is going to take the chef; you just know you need the onions and you can’t stop everything else you’re doing while you wait for them.
Concurrency is a complicated topic, but Objective C and Swift make great strides towards you not having to worry about it too much. But, making network requests are always going to be done asynchronously because you can't stop other things your app is doing in order to wait for an unknown amount of time for a request to finish. You don't cook in serial, a restaurant doesn't cook a single dish at a time, and your app will always be juggling multiple tasks.
At a very basic level, a network request follows the following steps:
Request
is madeResponse
is received after some time- Application responds to response (either
success
orfailure
) - Execution continues (another request could start, or an error alert shown if there was a problem)
As first covered in the "Closures In-depth" lesson, A common pattern to "listen" for network responses is through the use of closures, referred to as callbacks.
Closures in swift are first-class citizens which, among other things, Ymeans you can pass a closure as a parameter to another function. The twist here is that your closure is intended to be executed once your network request receives its response. So, it "waits" for the response to be retrieved while your app continues executing other code. The "lifetime" of the closure you pass into a function in this manner can "outlive" the "lifetime" of the function it's being passed to. In some ways, this closure enters a time machine where time stands still until the network request finishes. In the meanwhile, the rest of the function goes through each line of its code and finishes execution, unaware of the closure in the time machine. When the network call does finish, the callback gets the results and exits the time chamber.
Let's now look at how this affects our current project
Update getInstaCats(from:)
to:
// You need to include that `@escaping` keyword for callbacks
// if you forget it, don't worry, Xcode will give you an error and ask to correct it for you
func getInstaCats(from apiEndpoint: String, callback: @escaping ([InstaCat]?) -> Void) {
// other code
}
Now with the function updated to use a callback closure, we can finish:
func getInstaCats(from apiEndpoint: String, callback: @escaping ([InstaCat]?) -> Void) {
if let validInstaCatEndpoint: URL = URL(string: apiEndpoint) {
let session = URLSession(configuration: URLSessionConfiguration.default)
session.dataTask(with: validInstaCatEndpoint) { (data: Data?, response: URLResponse?, error: Error?) in
if error != nil {
print("Error encountered!: \(error!)")
}
if let validData: Data = data {
print(validData)
let allTheCats: [InstaCat]? = InstaCatParser().parseInstaCats(from: validData)
callback(allTheCats)
}
}
}.resume
}
To verify all is working, back in viewDidLoad
, update our code to use the new function's trailing closure syntax:
self.getInstaCats(from: instaCatEndpoint) { (instaCats: [InstaCat]?) in
if instaCats != nil {
DispatchQueue.main.async {
self.instaCats = instaCats!
self.tableview.reloadData()
}
}
}
Our newest addition to the getInstaCats
function, our @escaping
closure, ensures that we're able wait for the URLSession
data task to finish running completely before accessing its parsed [InstaCat]?
. The closure itself escapes the lifetime of the function call to getInstaCats
and remains around long enough for us to get [InstaCat]
.
Add a print
statement on the line just after }.resume
along with a print
statment just before you call callback(allTheCats)
. Check to see which one gets printed first to console. This should help illustrate how the closure "outlives" the function.
As we've learned, our callback extends the lifetime of the closure until at least the network requests finishes (in error or success). It's also what allows us to call this function from other classes. For this first exercise, refactor the code for getInstaCat(from:callback:)
and move it into InstaCatParser
.
Just because I like cats (like, I really like them), doesn't meaan you should always have to make a cat-related app. So, to give the InstaDog
s some love, we're going to give them their own section in our table. Here's what you need to be sure you do:
- Look at the content in
https://api.myjson.com/bins/58n98
- Or if the above isn't working, at
https://raw.githubusercontent.com/C4Q/AC3.2-NSURLSession/master/Resources/JSON/instadog.json
- Create a new model
struct InstaDog
- Refer to your tests for the properties and functions
InstaDog
should have (uncomment test code where relevant)
- Create a new class
class InstaDogParser
modeled afterInstaCatParser
- This class will have two functions:
func makeInstaDogs(apiEndpoint: String, callback: @escaping ([InstaDog]?) -> Void)
func getInstaDogs(from jsonData: Data) -> [InstaDog]?
- Make sure that you are able to get these classes to work properly (they should hit the endpoint, retrieve the data, and parse it out into 3
InstaDog
)
- In the
InstaCatTableViewController
in storyboard, add a new prototype cell (be sure to give it an identifier). This prototype cell should be of typesubtitle
- In
InstaCatTableViewController
, add new variables to keep track of: - Your new API endpoint
- Your new
[InstaDog]
- Your cell identifier from storyboard
- Update
numberOfRows
andnumberOfSections
so thatInstaCats
andInstaDogs
will be separated by section - Add the delegate function
titleForHeaderInSection
and give each section an appropriate name - Update
cellForRow
to check for theindexPath.section
and to use the correct prototype cell for each section.
- Take note that the
InstaDog
cells have an image and a specific format for theirdetailLabel
Your final version should look like this:
- In a real world scenario, it is possible that the
json
your app is going to receive ends up being malformed (perhaps the server team makes a change your app isn't ready for). Rewrite the implementation ofstruct InstaDog
to make itthrow
in the event that it cannot parse out thejson
.
- To test this, create your own mock
json
on myjson.com and change the endpoint for yourInstaDogFactory
to this newjson
.
- Sort the
InstaDog
section by eithernumberOfPosts
,followers
orfollowing
. You must accomplish the following:
- Create an
enum
calledInstaDogSort
with three cases:posts, followers, following
- Add a
static func
toInstaDog
that accepts parameters of[InstaDog]
andInstaDogSort
, and has a return of value type[InstaDog]