36.
URLSession
Written by Matthijs Hollemans & Fahim Farook
So far, you’ve used the Data(contentsOf:) method to perform the search on the iTunes web service. That is great for simple apps, but I want to show you another way to do networking that is more powerful.
iOS itself comes with a number of different classes for doing networking, from low-level sockets stuff that is only interesting to really hardcore network programmers, to convenient classes such as URLSession.
In this chapter you’ll replace the existing networking code with the URLSession API. That is the API the pros use for building real apps, but don’t worry, it’s not more difficult than what you’ve done before — just more powerful.
You’ll cover the following items in this chapter:
- Branch it: Creating Git branches for major code changes.
-
Put URLSession into action: Use the
URLSessionclass for asynchronous networking instead of downloading the contents of a URL directly. - Cancel operations: Canceling a running network request when a second network request is initiated.
- Search different categories: Allow the user to select a specific iTunes Store category to search in instead of returning items from all categories.
- Download the artwork: Download the images for search result items and display them as part of the search result listing.
- Merge the branch: Merge your changes from your working Git branch back to your main branch.
Branch it
Whenever you make a big change to the code — such as replacing all the networking stuff with URLSession — there is a possibility that you’ll mess things up. I certainly do often enough! That’s why it’s smart to create a Git branch first.
The Git repository contains a history of all the app’s code, but it can also contain this history along different paths.
You just finished the first version of the networking code and it works pretty well. Now you’re going to completely replace that with a — hopefully — better solution. In doing so, you may want to commit your progress at several points along the way.
What if it turns out that switching to URLSession wasn’t such a good idea after all? Then you’d have to restore the source code to a previous commit from before you started making those changes. In order to avoid this potential mess, you can make a branch instead.
Every time you’re about to add a new feature to your code or have a bug to fix, it’s a good idea to make a new branch and work on that. When you’re done and are satisfied that everything works as it should, merge your changes back into the main branch. Different people use different branching strategies but this is the general principle.
So far you have been committing your changes to the “main” branch. Now you’re going to make a new branch, let’s call it “urlsession”, and commit your changes to that. When you’re done with this new feature you will merge everything back into the main branch.
You can find the branches for your repository in the Source Control navigator:
➤ Select main — or whatever is your current branch — from the branch list, and right-click on the branch name to get a context-menu with possible actions. Select Branch from “main”…:
➤ You will get a dialog asking for the new branch name. Enter urlsession as the new name and click Create.
When Xcode is done, you’ll see that a new “urlsession” branch has been added and that it is now the current one.
This new branch contains the exact same source code and history as the main branch, or whichever branch you used as the parent for the new branch. But from here on out the two paths will diverge — any changes you make happen on the “urlsession” branch only.
Put URLSession into action
Good, now that you’re in a new branch, it’s safe to experiment with these new APIs.
➤ First, remove performStoreRequest(with:) from SearchViewController.swift. Yup, that’s right, you won’t be needing that method anymore.
Don’t be afraid to remove old code. Some developers only comment out the old code but leave it in the project, just in case they may need it again some day. You don’t have to worry about that because you’re using source control. Should you really need it, you can always find the old code in the Git history. Besides, if the experiment should fail, you can simply throw away this branch and switch back to the “original” one.
Anyway, on to URLSession. This is a closure-based API, meaning that instead of making a delegate, you pass it a closure containing the code that should be performed once the response from the server has been received. URLSession calls this closure the completion handler.
➤ Change searchBarSearchButtonClicked(_:) as follows:
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
if !searchBar.text!.isEmpty {
. . .
searchResults = []
// Replace all code after this with new code below
// 1
let url = iTunesURL(searchText: searchBar.text!)
// 2
let session = URLSession.shared
// 3
let dataTask = session.dataTask(with: url) {data, response, error in
// 4
if let error = error {
print("Failure! \(error.localizedDescription)")
} else {
print("Success! \(response!)")
}
}
// 5
dataTask.resume()
}
}
This is what the code does:
-
Create the
URLobject using the search text, just like before. -
Get a shared
URLSessioninstance, which uses the default configuration with respect to caching, cookies, and other web stuff.If you want to use a different configuration — for example, to restrict networking to when Wi-Fi is available but not when there is only cellular access — then you have to create your own
URLSessionConfigurationandURLSessionobjects. But for this app, the default one will be fine. -
Create a data task. Data tasks are for fetching the contents of a given URL. The code from the completion handler will be invoked when the data task has received a response from the server.
-
Inside the closure, you’re given three parameters:
data,response, anderror. These are all optionals so they can beniland have to be unwrapped before you can use them.If there was a problem, error contains an
Errorobject describing what went wrong. This happens when the server cannot be reached or the network is down or there is some other hardware failure.If
errorisnil, the communication with the server succeeded;responseholds the server’s response code and headers, anddatacontains the actual data fetched from the server, in this case a blob of JSON.For now, you simply use
print()to show success or failure. -
Finally, once you have created the data task, you need to call
resume()to start it. This sends the request to the server on a background thread. So, the app is immediately free to continue —URLSessionis as asynchronous as they come.
With these changes made, you can run the app and see what URLSession makes of it.
➤ Run the app and search for something. After a second or two you should see a Console message saying “Success!” followed by a dump of the HTTP response headers.
Excellent!
A brief review of closures
You’ve seen closures a few times now. They are a really powerful feature of Swift and you can expect to be using them all the time when you’re working with Swift code. So, it’s good to have at least a basic understanding of how they work.
A closure is simply a piece of source code that you can pass around just like any other type of object. The difference between a closure and regular code is that the code from the closure is not executed right away. Instead, it is stored in a “closure object” and can be executed at a later point, even more than once.
That’s exactly what URLSession does: it holds on to the “completion handler” closure and only performs it when a response is received from the web server or when a network error occurs.
While we used a trailing closure above, the same code could also be written like this:
let dataTask = session.dataTask(with: url, completionHandler: {
data, response, error in
. . . source code . . .
})
The thing behind completionHandler inside the { } brackets is the closure. The form of a closure is always:
{ parameters in
your source code
}
or without parameters:
{
your source code
}
Just like a method or function, a closure can accept parameters. They are separated from the source code by the “in” keyword. In URLSession’s completion handler the parameters are data, response, and error.
Thanks to Swift’s type inference, you don’t need to specify the data types of the parameters. However, you could write them out in full if you wanted to:
let dataTask = session.dataTask(with: url, completionHandler: {
(data: Data?, response: URLResponse?, error: Error?) in
. . .
})
Tip: For a parameter without the type annotation, you can Option-click in Xcode to find out what its type is. This trick works for any symbol in your code.
If you don’t use a particular parameter in your closure code you can substitute it with _, the wildcard symbol:
let dataTask = session.dataTask(with: url, completionHandler: {
data, _, error in
. . .
})
If a closure is really simple, you can leave out the parameter list altogether and use $0, $1, and so on as the parameter names.
let dataTask = session.dataTask(with: url, completionHandler: {
print("My parameters are \($0), \($1), \($2)")
})
You wouldn’t do that with URLSession’s completion handler, though. It’s much easier if you know the parameters are called data, response, and error than remembering what $0, $1, and $2 stand for.
As you’ve seen all the closure code in this book, if a closure is the last parameter of a method, you can use trailing syntax to simplify the code a little:
let dataTask = session.dataTask(with: url) {
data, response, error in
. . .
}
Now the closure appears after the closing parenthesis, not inside. Many people, myself included, find this more natural to read.
Closures are useful for other things too, such as initializing objects and lazy loading:
lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter
}()
The code to create and initialize the DateFormatter object sits inside a closure. The () at the end causes the closure to be evaluated and the returned object is put inside the dataFormatter variable. This is a common trick for placing complex initialization code right next to the variable declaration.
It’s no coincidence that closures look a lot like functions. In Swift, closures, methods, and functions are really all the same thing. For example, you can supply the name of a method or function when a closure is expected, as long as the parameters match:
let dataTask = session.dataTask(with: url, completionHandler: myHandler)
. . .
func myHandler(data: Data?, response: URLResponse?, error: Error?) {
. . .
}
The above somewhat negates one of the prime benefits of closures — keeping all the code in the same place — but there are situations where this is quite useful when the method acts as a “mini” delegate.
One final thing to be aware of with closures is that they capture any variables used inside the closure, including self. This can create ownership cycles, often leading to memory leaks. To avoid this, you can supply a capture list:
let dataTask = session.dataTask(with: url) {
[weak self] data, response, error in
. . .
}
Whenever you access a property or call a method, you’re implicitly using self. Inside a closure, however, Swift requires that you always write self. in front of the method or property name. This makes it clear that self is being captured by the closure:
let dataTask = session.dataTask(with: url) { data, response, error in
self.callSomeMethod() // self is required
}
SearchViewController doesn’t have to worry about URLSession capturing self because the data task is only short-lived, while the view controller sticks around for as long as the app itself. This ownership cycle is quite harmless. As you add more functionality to StoreSearch you will have to use [weak self] with URLSession or the app might crash and burn!
Note: Swift also has the concept of “no escape” closures. We won’t go into that here, except to mention that no-escape closures don’t capture
self, so you don’t have to write “self.” everywhere. Nice, but you can only use such closures under very specific circumstances!
Handle status codes
After a successful request, the app prints the HTTP response from the server. The response object might look something like this:
<NSHTTPURLResponse: 0x600003c7e6e0> { URL: https://itunes.apple.com/search?term=Knack&limit=200 } { Status Code: 200, Headers {
"Cache-Control" = (
"max-age=86400"
);
"Content-Disposition" = (
"attachment; filename=1.txt"
);
"Content-Encoding" = (
gzip
);
"Content-Length" = (
35427
);
"Content-Type" = (
"text/javascript; charset=utf-8"
);
Date = (
"Sat, 22 Aug 2020 12:15:42 GMT"
);
. . .
} }
If you’ve done any web development before, this should look familiar. These “HTTP headers” are always the first part of the response from a web server that precedes the actual data you’re receiving. The headers give additional information about the communication that just happened.
What you’re especially interested in is the status code. The HTTP protocol has defined a number of status codes that tell clients whether the request was successful or not. No doubt you’re familiar with 404, web page not found.
The status code you want to see is 200 OK, which indicates success — Wikipedia has the complete list of codes, wikipedia.org/wiki/List_of_HTTP_status_codes.
To make the error handling of the app a bit more robust, let’s check to make sure the HTTP response code really is 200. If not, something has gone wrong and we can’t assume that the received data contains the JSON we’re after.
➤ Change the contents of the completionHandler to:
if let error = error {
print("Failure! \(error.localizedDescription)")
} else if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
print("Success! \(data!)")
} else {
print("Failure! \(response!)")
}
The response parameter has the data type URLResponse, but that doesn’t have a property for the status code. Because you’re using the HTTP protocol, what you’ve really received is an HTTPURLResponse object, a subclass of URLResponse. So, first you cast it to the proper type, and then look at its statusCode property — you’ll consider the job a success only if the status code is 200.
Notice the use of the comma inside the if let statement to combine these checks into a single line. You could also have written it with a second if, but I find that harder to read:
} else if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode == 200 {
print("Success! \(data!)")
}
Whenever you need to unwrap an optional and also check the value of that optional, using if let …, … is the nicest way to do that.
➤ Run the app and search for something. You should now see something like:
Success! 295831 bytes
Since your received data is in the form of a Data object, unlike text, its content can’t be printed out. So, you just get the length of the data instead.
It’s always a good idea to actually test your error handling code. So, let’s first fake an error and get that out of the way.
➤ In iTunesURL(searchText:), change the URL string to:
"https://itunes.apple.com/searchLOL?term=%@&limit=200"
Here, I’ve changed the endpoint from search to searchLOL. It doesn’t really matter what you type there, as long as it’s something that cannot possibly exist on the iTunes server.
➤ Run the app again. Now a search should respond with something like this:
<NSHTTPURLResponse: 0x600002792b00> { URL: https://itunes.apple.com/searchLOL?term=Jango&limit=200 } { Status Code: 404, Headers {
"Cache-Control" = (
"private, max-age=300"
);
. . .
} }
As you can see, the status code is now 404 — there is no searchLOL page — and the app correctly considers this a failure. That’s a good thing too, because if you were to convert the value of data to text, data now contains the following:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL /searchLOL was not found on this server.</p>
</body></html>
That is definitely not JSON but HTML. If you tried to convert that into JSON objects, you’d fail horribly.
Great, so the error handling works! Let’s parse received JSON data.
Parse the data
➤ First, put iTunesURL(searchText:) back to the way it was — use ⌘+Z to undo.
➤ In the completionHandler, replace the print("Success! \(data)") line with:
if let data = data {
self.searchResults = self.parse(data: data)
self.searchResults.sort(by: <)
DispatchQueue.main.async {
self.isLoading = false
self.tableView.reloadData()
}
return
}
This unwraps the optional object from the data parameter and then calls parse(data:) to turn the dictionary’s contents into SearchResult objects, just like you did before. Finally, you sort the results and put everything into the table view. This should look very familiar.
It’s important to realize that the completion handler closure won’t be performed on the main thread. Because URLSession does all the networking asynchronously, it will also call the completion handler on a background thread.
Parsing the JSON and sorting the list of search results could potentially take a while — not seconds but possibly long enough to be noticeable. You don’t want to block the main thread while that is happening, so it’s preferable that this happens in the background too.
But when the time comes to update the UI, you need to switch back to the main thread — them’s the rules. That’s why you wrap the reloading of the table view in a DispatchQueue.main.async closure.
If you forget to do this, your app may still appear to work. That’s the insidious thing about working with multiple threads. However, it may also crash in all kinds of mysterious ways. So remember, UI stuff should always happen on the main thread. Write it on a Post-It note and stick it to your screen!
➤ Run the app. The search should work again. You have successfully replaced the old networking code with URLSession!
Tip: If you want to determine via code whether a particular piece of code is being run on the main thread or not, add the following code snippet:
print("On main thread? " + (Thread.current.isMainThread ? "Yes" : "No"))Go ahead, paste this at the top of the
completionHandlerclosure and see what it says.Of course, the official framework documentation should be your first stop. Usually when a method takes a closure, the docs mention whether it is performed on the main thread or not. But if you’re not sure, or just can’t find it in the docs, add the above
print()and be enlightened.
Handle errors
➤ At the very end of the completion handler closure, below the if statements, add the following:
DispatchQueue.main.async {
self.hasSearched = false
self.isLoading = false
self.tableView.reloadData()
self.showNetworkError()
}
The code execution reaches here only if something went wrong. You call showNetworkError() to let the user know about the problem.
Note that you do tableView.reloadData() here too, because the contents of the table view need to be refreshed to get rid of the Loading… indicator. And of course, all this happens on the main thread.
Exercise. Why doesn’t the error alert show up on success? After all, the above piece of code sits at the bottom of the closure, so doesn’t it always get executed?
Answer: Upon successfully loading the data, the return statement exits the closure after the search results get displayed in the table view. So in that case, execution never reaches the bottom of the closure.
➤ Fake an error situation to test that the error handling code really works.
Testing errors is not a luxury! The last thing you want is for your app to crash when a networking error occurs because of faulty error handling code. I’ve worked on codebases where it was obvious the previous developer never bothered to verify that the app was able to recover from errors — that’s probably why they were the previous developer.
Things will go wrong in the wild and your app better be prepared to deal with it. As the MythBusters say, “failure is always an option”.
Does the error handling code work? Great! Time to add some new networking features to the app.
➤ This is a good time to commit your changes. Remember, this commit only happens on the “urlsession” branch, not on the main branch.
Cancel operations
What happens when a search takes a long time and the user starts a second search while the first one is still going? The app doesn’t disable the search bar, so it’s possible for the user to do this. When dealing with networking — or any asynchronous process, really — you have to think these kinds of situations through.
There is no way to predict what happens, but it will most likely be a strange experience for the user. They might see the results from their first search, which they are no longer expecting, only for that to be replaced by the results of the second search a few seconds later. Confusing!
But there is no guarantee the first search completes before the second, so the results from search #2 may arrive first and then get overwritten by the results from search #1, which is definitely not what the user wanted to see either.
Because you’re no longer blocking the main thread, the UI always accepts user input, and you cannot assume the user will sit still and wait until the request is done.
You can usually fix this in one of two ways:
-
Disable all controls. The user cannot tap anything while the operation is taking place. This does not mean you’re blocking the main thread; you’re just making sure the user cannot mess up the order of things.
-
Cancel the on-going request when the user initiates a new request.
For this app, you’re going to pick the second solution because it makes for a nicer user experience. Every time the user performs a new search, you cancel the previous request. URLSession makes this easy: data tasks have a cancel() method.
When you created the data task, you were given a URLSessionDataTask object, and you placed this into a local constant named dataTask. Cancelling the task, however, needs to happen the next time searchBarSearchButtonClicked(_:) is called.
Storing the URLSessionDataTask object into a local variable isn’t good enough anymore; you need to keep that reference beyond the scope of the method. In other words, you have to store it in an instance variable.
➤ Add the following instance variable to SearchViewController.swift:
var dataTask: URLSessionDataTask?
This is an optional because you won’t have a data task until the user performs a search.
➤ In searchBarSearchButtonClicked(_:), remove let from the line that creates the new data task object:
dataTask = session.dataTask(with: url, completionHandler: {
You’ve removed the let keyword because dataTask should no longer be a local; it now refers to the instance variable.
➤ At the end of the method, add a question mark to the line that starts the task:
dataTask?.resume()
Because dataTask is an optional, you have to unwrap the optional somehow before you can use it. Here you’re using optional chaining.
➤ Finally, near the top of the method before you set isLoading to true, add:
dataTask?.cancel()
If there is an active data task, this cancels it, making sure that no old searches can ever get in the way of the new search.
Thanks to the optional chaining, if no search has been done yet and dataTask is still nil, this simply ignores the call to cancel(). You could also unwrap the optional with if let, but using the question mark is shorter and just as safe.
Exercise. Why can’t you write
dataTask!.cancel()to unwrap the optional?
Answer: If an optional is nil, using ! will crash the app. You’re only supposed to use ! to unwrap an optional when you’re sure it won’t be nil. But the very first time the user types something into the search bar, dataTask will still be nil and using ! is not a good idea.
➤ Test the app with and without this call to dataTask.cancel() to experience the difference.
Use the Network Link Conditioner preferences pane to delay each query by a few seconds so it’s easier to get two requests running at the same time.
Hmm… you may notice something odd. When the data task gets cancelled, you get the network error popup.
As it turns out, when a data task gets cancelled, its completion handler is still invoked but with an Error object that has error code -999. That’s what caused the error alert to pop up.
You’ll have to make the error handler a little smarter to ignore code -999. After all, the user cancelling the previous search is no cause for panic.
➤ In the completionHandler, change the if let error section to:
if let error = error as NSError?, error.code == -999 {
return // Search was cancelled
} else if let httpResponse = . . .
This simply ends the closure when there is an error with code -999. The rest of the closure gets skipped.
➤ If you’re satisfied it works, commit the changes to the repository.
Note: Maybe you don’t think it’s worth making a commit when you’ve only changed a few lines, but many small commits are often better than a few big ones. Each time you fix a bug or add a new feature, it is a good time to commit.
Search different categories
The iTunes store has a vast collection of products and each search returns at most 200 items. It can be hard to find what you’re looking for by name alone. So, you’ll add a control to the screen that lets users pick the category they want to search in. It will look like this:
This type of control is called a segmented control and is used to pick one option out of a set of choices.
Add the segmented control
➤ Open the storyboard. Drag a new Toolbar into the view and put it below the Search Bar. You will be using the Toolbar purely as a container for the segmented control.
Make sure the Toolbar doesn’t get added inside the Table View. It may be easiest to drag it from the Objects Library directly into the Document Outline and drop it below the Search Bar. Then change its Y-position to 56.
➤ The Toolbar comes with an item in it. Select the item and delete it. If you find this hard to do on the canvas, select the item via the Document Outline and then delete it.
➤ With the Toolbar selected, open the Add New Constraints menu and pin its top, left, and right sides.
➤ Drag a new Segmented Control from the Objects Library on to the Toolbar.
The design should now look like this:
➤ Select the Segmented Control — you might need to use the Document Outline again since the Segmented Control gets embedded in a Bar Button Item when you place it in the Toolbar — and in the Attributes inspector, set the number of segments to 4.
➤ Change the title of the first segment to All. Then select the second segment via the Segment dropdown and set its title to Music. The title for the third segment should be Software and the fourth segment is E-books.
Note: You can also change the segment title by double-clicking inside the segment.
The scene should look like this now:
Next, you’ll add a new outlet and action method for the Segmented Control. This is a good opportunity to practice using the Assistant editor.
Use the assistant editor
➤ Press Control+Option+⌘+Enter to open the Assistant editor and then Control-drag from the Segmented Control into the view controller source code to add the new outlet:
@IBOutlet weak var segmentedControl: UISegmentedControl!
To add the action method you can also use the Assistant editor. Control-drag from the Segmented Control into the source code again, but this time choose the following:
- Connection: Action
- Name: segmentChanged
- Type: UISegmentedControl
- Event: Value Changed
- Arguments: Sender
➤ Press Connect to add the action method. Then, add a print() statement to the new method:
@IBAction func segmentChanged(_ sender: UISegmentedControl) {
print("Segment changed: \(sender.selectedSegmentIndex)")
}
Type ⌘+Enter to close the Assistant editor. These are very handy keyboard shortcuts to remember.
➤ Run the app to make sure everything still works. Tapping a segment should log a number — the index of that segment — to the Console.
Use the segmented control
Notice that the first row of the table view is partially obscured again. Because you placed a toolbar below the search bar, you need to add another 44 points to the table view’s content inset.
➤ Change that line in viewDidLoad() to:
tableView.contentInset = UIEdgeInsets(top: 94, left: 0, . . .
You will be using the segmented control in two ways. First of all, it determines what sort of products the app will search for. Second, if you have already performed a search and you tap on one of the other segment buttons, the app will search again for the new product category.
That means a search can now be triggered by two different events: tapping the Search button on the keyboard and selecting an item in the Segmented Control.
➤ Rename the searchBarSearchButtonClicked(_:) method to performSearch() and remove the searchBar parameter.
You’re doing this to put the search logic into a separate method that can be invoked from more than one place. Removing searchBar as the parameter of this method is no problem because there is also an @IBOutlet property with that name and any references to searchBar in performSearch() will simply use that property.
➤ Now add a new version of searchBarSearchButtonClicked(_:) to the source code:
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
performSearch()
}
➤ Also replace the segmentChanged(_:) action method with:
@IBAction func segmentChanged(_ sender: UISegmentedControl) {
performSearch()
}
➤ Run the app and verify that searching still works. When you tap on the different segments, the search should be performed again as well.
Note: The second time you search for the same thing, the app may return results very quickly. The networking layer is now returning a cached response so it doesn’t have to download the whole thing again, which is usually a performance gain on mobile devices. However, there is an API to turn off this caching behavior if that makes sense for your app.
There is one thing left to be done — you have to tell the app to use the category based on the selected segment for the search. You’ve already seen that you can get the index of the selected segment with the selectedSegmentIndex property. This returns an Int value (0, 1, 2, or 3).
➤ Change the iTunesURL(searchText:) method so that it accepts this Int as a parameter and then builds up the request URL accordingly:
func iTunesURL(searchText: String, category: Int) -> URL {
let kind: String
switch category {
case 1: kind = "musicTrack"
case 2: kind = "software"
case 3: kind = "ebook"
default: kind = ""
}
let encodedText = searchText.addingPercentEncoding(
withAllowedCharacters: CharacterSet.urlQueryAllowed)!
let urlString = "https://itunes.apple.com/search?" +
"term=\(encodedText)&limit=200&entity=\(kind)"
let url = URL(string: urlString)
return url!
}
This first turns the category index from a number into a string, kind. Note that the category index is passed to the method as a new parameter.
Then it puts this string behind the &entity= parameter in the URL. For the “All” category, the entity value is empty, but for the other categories it is “musicTrack”, “software”, and “ebook”, respectively. Also note that instead of calling String(format:), you now construct the URL string using string interpolation.
➤ In performSearch(), change the line that gets the URL to the following:
let url = iTunesURL(
searchText: searchBar.text!,
category: segmentedControl.selectedSegmentIndex)
And that should do it!
Note: You could have used
segmentedControl.selectedSegmentIndexdirectly insideiTunesURLinstead of passing the category index as a parameter. Using the parameter is the better design, though. It makes it possible to reuse the same method with a different type of control, should you decide that a Segmented Control isn’t really the right component for this app. It is always a good idea to make methods as independent from each other as possible.
➤ Run the app and search for “stephen king”. In the All category that gives results for anything from songs to movies to podcasts to audio books. But if all you wanted were to get to his books, you can now use the E-Books category to finally find some of his novels.
This finalizes the UI design of the main screen. This is as good a point as any to replace the empty launch screen from the template.
Set the launch screen
➤ Remove the LaunchScreen.storyboard file from the project.
➤ In the Project Settings screen, under App Icons and Launch Images, change Launch Screen File to Main.
Now when the app starts, it uses the initial view controller from the storyboard as the launch image. Also verify that the app works properly on the iPad simulator and the larger iPhone models.
➤ Commit the changes and get ready for some more networking!
Download the artwork
The JSON search results contain a number of URLs to images and you put two of those — imageSmall and imageLarge — into the SearchResult object. Now you are going to download these images over the Internet and display them in the table view cells.
Downloading images, just like using a web service, is simply a matter of doing an HTTP request to a server that is connected to the Internet. An example of such a URL is:
Click that link and it will open the picture in a new web browser window. The server where this picture is stored is not itunes.apple.com but is2-ssl.mzstatic.com, but that doesn’t matter at all to the app. As long as it has a valid URL, the app will just go fetch the file at that location, no matter where it is and what kind of file it is.
There are various ways that you can download files from the Internet. You’re going to use URLSession and write a handy UIImageView extension to make this really convenient. Of course, you’ll be downloading these images asynchronously!
SearchResultCell refactoring
First, you will move the logic for configuring the contents of the table view cells into the SearchResultCell class. That’s a better place for it. Logic related to an object should live inside that object as much as possible, not somewhere else.
Many developers have a tendency to stuff everything into their view controllers, but if you can move some of the logic into other objects, that makes for a much cleaner program.
➤ Add the following method to SearchResultCell.swift:
// MARK: - Helper Methods
func configure(for result: SearchResult) {
nameLabel.text = result.name
if result.artist.isEmpty {
artistNameLabel.text = "Unknown"
} else {
artistNameLabel.text = String(format: "%@ (%@)", result.artist, result.type)
}
}
This is basically the same code as in tableView(_:cellForRowAt:).
➤ Now, change tableView(_:cellForRowAt:) as follows:
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if isLoading {
. . .
} else if searchResults.count == 0 {
. . .
} else {
. . .
let searchResult = searchResults[indexPath.row]
// Replace all code after this with new code below
cell.configure(for: searchResult)
return cell
}
}
This small refactoring of moving some code from one class, SearchViewController, into another, SearchResultCell, was necessary to make the next bit work right.
In hindsight, it makes more sense to do this sort of thing in SearchResultCell anyway, but until now it did not really matter. Don’t be afraid to refactor your code! Remember, if you screw up, you can always go back to your last Git commit.
➤ Run the app to make sure everything still works fine.
UIImageView extension for downloading images
OK, here comes the cool part. You will now add an extension for UIImageView that downloads the image and automatically displays it via the image view on the table view cell with just one line of code!
As you know, an extension can be used to extend the functionality of an existing class without having to subclass it. This works even for classes from system frameworks.
UIImageView doesn’t have built-in support for downloading images, but this is a very common thing to do in apps. It’s great that you can simply plug in your own extension and from then on every UIImageView in your app has this new ability.
➤ Add a new file to the project using the Swift File template, and name it UIImageView+DownloadImage.swift.
➤ Replace the contents of the new file with the following:
import UIKit
extension UIImageView {
func loadImage(url: URL) -> URLSessionDownloadTask {
let session = URLSession.shared
// 1
let downloadTask = session.downloadTask(with: url) {
[weak self] url, _, error in
// 2
if error == nil, let url = url,
let data = try? Data(contentsOf: url), // 3
let image = UIImage(data: data) {
// 4
DispatchQueue.main.async {
if let weakSelf = self {
weakSelf.image = image
}
}
}
}
// 5
downloadTask.resume()
return downloadTask
}
}
This should look very similar to what you did before with URLSession, but there are some differences:
-
After obtaining a reference to the shared
URLSession, you create a download task. This is similar to a data task, but it saves the downloaded file to a temporary location on disk instead of keeping it in memory. -
Inside the completion handler for the download task, you’re given a URL where you can find the downloaded file — this URL points to a local file rather than an internet address. Of course, you must also check that
errorisnilbefore you continue. -
With this local URL you can load the file into a
Dataobject and then create an image from that. It’s possible that constructing theUIImagefails, for example, when what you downloaded was not a valid image but a 404 page or something else unexpected. As you can tell, when dealing with networking code, you need to check for errors every step of the way! -
Once you have the image, you can put it into the
UIImageView’simageproperty. Because this is UI code you need to do this on the main thread.Here’s the tricky thing: it is theoretically possible that the
UIImageViewno longer exists by the time the image arrives from the server. After all, it may take a few seconds and the user might have navigated away to a different part of the app by then.That won’t happen in this part of the app because the image view is part of a table view cell and they get recycled but not thrown away. But later on you’ll use this same code to load an image on a screen that may be closed while the image file is still downloading. In that case, you don’t want to set the image if the
UIImageViewis not visible anymore.That’s why the capture list for this closure includes
[weak self], whereselfnow refers to theUIImageView. Inside theDispatchQueue.main.asyncyou need to check whether “self” still exists; if not, then there is no moreUIImageViewto set the image on. -
After creating the download task, you call
resume()to start it, and then return theURLSessionDownloadTaskobject to the caller. Why return it? That gives the app the opportunity to callcancel()on the download task if necessary. You’ll see how that works in a minute.
And that’s all you need to do. From now on you can call loadImage(url:) on any UIImageView object in your project. Cool, huh?
Note: Swift lets you combine multiple
if letstatements into a single line, like you did above:
if error == nil, let url = …, let data = …, let image = … {There are three optionals being unwrapped here: 1)
url, 2) the result fromData(contentsOf:), and 3) the result fromUIImage(data:).You can write this as three separate
if letstatements, and one forif error == nil, but I find that having everything inside a singleifstatement is easier to read than many nestedifstatements spread over several lines.
Use the image downloader extension
➤ Switch to SearchResultCell.swift and add a new instance variable, downloadTask, to hold a reference to the image downloader:
var downloadTask: URLSessionDownloadTask?
➤ Now, add the following lines to the end of configure(for:):
artworkImageView.image = UIImage(systemName: "square")
if let smallURL = URL(string: result.imageSmall) {
downloadTask = artworkImageView.loadImage(url: smallURL)
}
This tells the UIImageView to load the image from imageSmall and to place it in the cell’s image view. While the real artwork is downloading, the image view displays a placeholder image — the same symbol image as the one one from the nib for this cell.
Note: You can load an image from the asset catalog by using
UIImage(named:)while you can load a symbol image usingUIImage(systemName:)– note that the parameter name is different for each type of use.
➤ Run the app and enjoy your colorful images!
App transport security
While your image downloading experience worked great here, sometimes when dealing with image downloads, or accessing any web URL for that matter, you might see something like the following in the Xcode Console, along with a ton of error messages like the following for failed download tasks:
The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.
While you could previously download images over HTTP, you can no longer do so. Instead, you always need to use HTTPS.
If for some reason you do need to access files over HTTP in your app, you can add a key to the app’s Info.plist to bypass this App Transport Security feature, allowing you to use plain http:// URLs.
➤ Open Info.plist and add a new row. Choose App Transport Security Settings from the list of keys.
➤ Make sure the Type is a Dictionary.
➤ Add a new row inside that dictionary and choose Allow Arbitrary Loads from the list. Set it to YES.
That’s all you need to do to access HTTP links. However, you’re only supposed to bypass App Transport Security if there is absolutely no way you can make the app work over HTTPS. If you’re making an app that talks to a server you control, then the best thing to do is to enable HTTPS on the server, not disable HTTPS in the app.
The Info.plist setting is only intended for when you need to communicate with other people’s servers that do not support HTTPS. Obviously, in that case, the app should not send sensitive data to those servers! Unprotected HTTP should only be used for downloading publicly accessible data, such as images.
When you set the Allow Arbitrary Loads key to YES, the app can use any URL that starts with http://, regardless of the domain. To allow HTTP on specific domains only, set to Allow Arbitrary Loads to NO and add a new row under the App Transport Security Settings dictionary and select Exception Domains from the list of keys.
The value for the new Exception Domains key is a dictionary. Under that dictionary, you can add a new dictionary for each domain.
For example, the iTunes web service appears to host all its preview images on the website mzstatic.com. You could configure Info.plist as follows:
Now the app only allows http:// requests from mzstatic.com and any of its subdomains, but requires https:// URLs for any other domains.
Note that Apple has indicated that this ability to bypass App Transport Security (ATS) will be removed at some time in the future. So do not rely on the ATS-bypass being something which would always be available.
Cancel previous image downloads
These images already look pretty sweet, but you’re not quite done yet. Remember that table view cells can be reused, so it’s theoretically possible that you’re scrolling through the table and some cell is about to be reused while its previous image is still downloading.
You no longer need that image, so you should really cancel the pending download. Table view cells have a special method named prepareForReuse() that is ideal for this.
➤ Add the following method to SearchResultCell.swift:
override func prepareForReuse() {
super.prepareForReuse()
downloadTask?.cancel()
downloadTask = nil
}
You simply cancel any image download that is still in progress.
Exercise. Put a
print()in theprepareForReuse()method and see if you can trigger it.
On a decent Wi-Fi connection, loading the images is very fast. You almost cannot see it happen, even if you scroll quickly. It also helps that the image files are small — only 60 by 60 pixels — and that the iTunes servers are fast.
That is key to having a snappy app: don’t download more data than you need.
Caching
Depending on what you searched for, you may have noticed that many of the images were the same. For example, you might get many identical album covers in the search results. URLSession is smart enough not to download identical images — or at least images with identical URLs — twice. That principle is called caching and it’s very important on mobile devices.
Mobile developers are always trying to optimize their apps to do as little as possible. If you can download something once and then use it over and over, that’s a lot more efficient than re-downloading it all the time.
Images aren’t the only things that you can cache. You can also cache the results of big computations, for example. Or views, as you have been doing in the previous apps, probably without even realizing it. When you use the principle of lazy loading, you delay the creation of an object until you need it and then you cache it for the next time.
Cached data does not stick around forever. When your app gets a memory warning, it’s a good idea to remove any cached data that you don’t need right away. That means you will have to reload that data when you need it again later, but that’s the price you have to pay. For URLSession this is completely automatic, so that takes another burden off your shoulders.
Some caches are in-memory — the cached data only stays in the computer’s working memory. But it is also possible to cache the data to the disk. Your app even has a special directory for it, Library/Caches.
The caching policy used by StoreSearch is very simple — it uses the default settings. But you can configure URLSession to be much more advanced. Look into the documentation for URLCache and URLSessionConfiguration to learn more.
Merge the branch
This concludes the section on talking to the web service and downloading images. Later on, you’ll tweak the web service requests a bit more to include the user’s language and country, but for now, you’re done with this feature. I hope you got a good glimpse of what is possible with web services and how easy it is to build this functionality into your apps using URLSession.
➤ Commit these latest changes to the repository.
Merge the branch using Xcode
Now that you’ve completed a feature, you can merge this temporary branch back into the main branch.
➤ Switch to the Source Control navigator, select the main branch — or whatever was your main branch previously — under branches, and right-click to get the context menu of actions.
➤ Select Checkout… to switch your active branch back to the main branch.
➤ Next, right-click on the urlsession branch to get the context menu again and select Merge “urlsession” into “main”…:
➤ You’ll get a confirmation dialog. Click Merge if you want to continue.
Now that the main branch is up-to-date with the networking changes, if you wanted to, you could remove the “urlsession” branch. Or, you could keep it and do more work on it later.
Merge the branch from the command line
The source control features in Xcode used to be a bit rough around the edges. So, it was possible that certain commands, especially merging changes, might not work correctly. If Xcode didn’t want to cooperate when you tried to merge changes, here is how you’d do it from the command line.
➤ First close Xcode. You don’t want to do any of this while Xcode still has the project open. That’s just asking for trouble.
➤ Open a Terminal, cd to the StoreSearch folder, and type the following commands:
git stash
This moves any unsaved files out of the way — no, it doesn’t have anything to do with facial hair :] This saves any uncommitted changes so you can later restore them, if need be.
git checkout main
This switches the current branch back to the main branch.
git merge urlsession
This merges the changes from the “urlsession” branch back into the main branch. If you get an error message at this point, then simply do git stash again and repeat the git merge command.
By the way, you don’t really need to keep those stashed files around, so if you want to remove them from your repository, you can do git stash drop. If you stashed twice, you also need to drop twice.
➤ Open the project again in Xcode. Now you’re back at the main branch and it also has the latest networking changes.
➤ Build and run to see if everything still works.
Git is a pretty awesome tool, but it takes a while to get familiar with it. Xcode’s Git support has improved a lot over the years, but for more complex things you might still need to use the command line — it’s well worth learning!
Note: Even though
URLSessionis pretty easy to use and quite capable, many developers prefer to use third-party networking libraries that are often even more convenient and powerful.One of the most popular native Swift libraries at this point is Alamofire (github.com/Alamofire/Alamofire).
I suggest you check out some of these libraries and see how you like them. Networking is such an important feature of mobile apps that it’s worth being familiar with the different possible approaches to send data up and down the ’net.
You can find the project files for this chapter under 36-URLSession in the Source Code folder.