Whether an app retrieves application data from a server, updates your social media status or downloads remote files to disk, it’s the HTTP network requests living at the heart of mobile applications that make the magic happen. To help you with the numerous requirements for network requests, Apple provides NSURLSession
, which is a complete suite of networking API methods for uploading and downloading content via HTTP.
In this NSURLSession
tutorial, you’ll learn how to use NSURLSession
to build the Half Tunes app, which lets you query the iTunes Search API and download 30-second previews of selected songs. The finished app will also support background transfers and let the user pause, resume or cancel in-progress downloads.
Getting Started
Download the starter project; it already contains a user interface for searching for songs and displaying search results, as well as some helper methods to parse JSON and play tracks. This lets you focus on implementing the networking aspects of the app.
Build and run your project; you should see a view with a search bar at the top and an empty table view below:
Type a query in the search bar and tap Search. The view remains empty, but don’t worry; you’ll change this with your new NSURLSession
calls.
Overview of NSURLSession
Before you begin, it’s important to appreciate NSURLSession
and its constituent classes, so take a minute to walk through the quick overview below.
NSURLSession
is technically both a class and a suite of classes for handling HTTP/HTTPS-based requests:
NSURLSession
is the key object responsible for sending and receiving HTTP requests. You create it via NSURLSessionConfiguration
, which comes in three flavors:
defaultSessionConfiguration
: Creates a default configuration object that uses the disk-persisted global cache, credential and cookie storage objects.ephemeralSessionConfiguration
: Similar to the default configuration, except that all session-related data is stored in memory. Think of this as a “private” session.backgroundSessionConfiguration
: Lets the session perform upload or download tasks in the background. Transfers continue even when the app itself is suspended or terminated.
NSURLSessionConfiguration
also lets you configure session properties such as timeout values, caching policies and additional HTTP headers. Refer to the documentation for a full list of configuration options.
NSURLSessionTask
is an abstract class that denotes a task object. A session creates a task, which does the actual work of fetching data and downloading or uploading files.
There are three types of concrete session tasks in this context:
NSURLSessionDataTask
: Use this task for HTTP GET requests to retrieve data from servers to memory.NSURLSessionUploadTask
: Use this task to upload a file from disk to a web service, typically via a HTTP POST or PUT method.NSURLSessionDownloadTask
: Use this task to download a file from a remote service to a temporary file location.
You can also suspend, resume and cancel tasks. NSURLSessionDownloadTask
has the additional ability to pause for future resumption.
Generally, NSURLSession
returns data in two ways: via a completion handler when a task finishes either successfully or with an error, or by calling methods on a delegate that you set upon session creation.
Now that you have an overview of what NSURLSession
can do, you’re ready to put the theory into practice!
Querying for Tracks
You’ll start by adding the code to query the iTunes Search API when the user searches for a track.
In SearchViewController.swift, add the following code to the top of the class:
// 1 let defaultSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration()) // 2 var dataTask: NSURLSessionDataTask? |
Here’s what you’re doing in the above code:
- You create a
NSURLSession
and initialize it with a default session configuration. - You declare a
NSURLSessionDataTask
variable which you’ll used to make an HTTP GET request to the iTunes Search web service when the user performs a search. The data task will be re-initialized and reused each time the user creates a new query.
Now, replace searchBarSearchButtonClicked(_:)
with the following:
func searchBarSearchButtonClicked(searchBar: UISearchBar) { dismissKeyboard() if !searchBar.text!.isEmpty { // 1 if dataTask != nil { dataTask?.cancel() } // 2 UIApplication.sharedApplication().networkActivityIndicatorVisible = true // 3 let expectedCharSet = NSCharacterSet.URLQueryAllowedCharacterSet() let searchTerm = searchBar.text!.stringByAddingPercentEncodingWithAllowedCharacters(expectedCharSet)! // 4 let url = NSURL(string: "https://itunes.apple.com/search?media=music&entity=song&term=\(searchTerm)") // 5 dataTask = defaultSession.dataTaskWithURL(url!) { data, response, error in // 6 dispatch_async(dispatch_get_main_queue()) { UIApplication.sharedApplication().networkActivityIndicatorVisible = false } // 7 if let error = error { print(error.localizedDescription) } else if let httpResponse = response as? NSHTTPURLResponse { if httpResponse.statusCode == 200 { self.updateSearchResults(data) } } } // 8 dataTask?.resume() } } |
Taking each numbered comment in turn:
- Upon each user query, you check if the data task is already initialized. If so, you cancel the task as you want to reuse the data task object for the latest query.
- You enable the network activity indicator on the status bar to indicate to the user that a network process is running.
- Before passing the user’s search string as a parameter to the query URL, you call
stringByAddingPercentEncodingWithAllowedCharacters(_:)
on the string to ensure that it’s properly escaped. - Next you construct a
NSURL
by appending the escaped search string as a GET parameter to the iTunes Search API base url. - From the session you created, you initialize a
NSURLSessionDataTask
to handle the HTTP GET request. The constructor ofNSURLSessionDataTask
takes in theNSURL
that you constructed along with a completion handler to be called when the data task is completed. - Upon receiving a callback that the task completed, you hide the activity indicator and invoke the UI update in the main thread.
- If the HTTP request is successful, you call
updateSearchResults(_:)
, which parses the responseNSData
intoTrack
s and updates the table view. - All tasks start in a suspended state by default; calling
resume()
starts the data task.
Build and run your app; search for any song and you should see the table view populate with the relevant track results like so:
With a little bit of NSURLSession
magic added, Half Tunes is now somewhat functional!
Downloading a Track
Being able to view song results is nice, but wouldn’t it be better if you could tap on a song to download it? That’s precisely your next order of business.
To make it easy to handle multiple downloads, you’ll first create a custom object to hold the state of an active download.
Create a new file named Download.swift in the Data Objects group.
Open Download.swift and add the following implementation:
class Download: NSObject { var url: String var isDownloading = false var progress: Float = 0.0 var downloadTask: NSURLSessionDownloadTask? var resumeData: NSData? init(url: String) { self.url = url } } |
Here’s a rundown of the properties of Download
:
- url: The URL of the file to download. This also acts as a unique identifier for a
Download
. - isDownloading: Whether the download is ongoing or paused.
- progress: The fractional progress of the download; a float between 0.0 and 1.0.
- downloadTask: The
NSURLSessionDownloadTask
that downloads the file. - resumeData: Stores the
NSData
produced when you pause a download task. If the host server supports it, you can use this to resume a paused download in the future.
Switch to SearchViewController.swift and add the following code to the top of your class:
var activeDownloads = [String: Download]() |
This simply maintains a mapping between URLs and their active Download
, if any.
Creating a Download Task
With all the preparatory work out of the way, you’re now ready to implement file downloads. You’ll first create a dedicated session to handle your download tasks.
In SearchViewController.swift, add the following code right before viewDidLoad()
:
lazy var downloadsSession: NSURLSession = { let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil) return session }() |
Here you initialize a separate session with a default configuration to handle all your download tasks. You also specify a delegate, which lets you receive NSURLSession
events via delegate calls. This is useful for tracking not just when a task is complete, but also the progress of the task.
Setting the delegate queue to nil
causes the session to create a serial operation queue, by default, to perform all calls to the delegate methods and completion handlers.
Note the lazy creation of downloadsSession
: this lets you delay the creation of the session until it’s needed. Most importantly, it lets you pass self
as the delegate parameter to the initializer — even if self
isn’t initialized.
In SearchViewController.swift, find the empty NSURLSessionDownloadDelegate
extension
extension SearchViewController: NSURLSessionDownloadDelegate { func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) { print("Finished downloading.") } } |
NSURLSessionDownloadDelegate
defines delegate methods that you need to implement when using NSURLSession
download tasks. The only non-optional method is URLSession(_:downloadTask:didFinishDownloadingToURL:)
, which you call whenever a download finishes. For now, you’ll print a simple message whenever a download completes.
With your session and delegate configured, you’re finally ready to create a download task when the user requests a track download.
In SearchViewController.swift, replace startDownload(_:)
with the following implementation:
func startDownload(track: Track) { if let urlString = track.previewUrl, url = NSURL(string: urlString) { // 1 let download = Download(url: urlString) // 2 download.downloadTask = downloadsSession.downloadTaskWithURL(url) // 3 download.downloadTask!.resume() // 4 download.isDownloading = true // 5 activeDownloads[download.url] = download } } |
When you tap the Download button for a track, you call startDownload(_:)
with the corresponding Track
. Here’s what’s going on :
- You first initialize a
Download
with the preview URL of the track. - Using your new session object, you create a
NSURLSessionDownloadTask
with the preview URL, and set it to thedownloadTask
property of theDownload
. - You start the download task by calling
resume()
on it. - You indicate that the download is in progress.
- Finally, you map the download URL to its
Download
in theactiveDownloads
dictionary.
Build and run your app; search for any track and tap the Download button on a cell. You should see a message printed on your console after a while, signifying that the download is complete.
Saving and Playing the Track
When a download task completes, URLSession(_:downloadTask:didFinishDownloadingToURL:)
provides a URL to the temporary file location. Your job is to move it to a permanent location in your app’s sandbox container directory before you return from the method. As well, you’ll have to remove the active download from the dictionary and update the table view.
You’ll add a helper method to make things easy. In SearchViewController.swift, add the following method to the class:
func trackIndexForDownloadTask(downloadTask: NSURLSessionDownloadTask) -> Int? { if let url = downloadTask.originalRequest?.URL?.absoluteString { for (index, track) in searchResults.enumerate() { if url == track.previewUrl! { return index } } } return nil } |
This method simply returns the the index of the Track
in the searchResults
list that has the given URL.
Next, replace URLSession(_:downloadTask:didFinishDownloadingToURL:)
with the following code:
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) { // 1 if let originalURL = downloadTask.originalRequest?.URL?.absoluteString, destinationURL = localFilePathForUrl(originalURL) { print(destinationURL) // 2 let fileManager = NSFileManager.defaultManager() do { try fileManager.removeItemAtURL(destinationURL) } catch { // Non-fatal: file probably doesn't exist } do { try fileManager.copyItemAtURL(location, toURL: destinationURL) } catch let error as NSError { print("Could not copy file to disk: \(error.localizedDescription)") } } // 3 if let url = downloadTask.originalRequest?.URL?.absoluteString { activeDownloads[url] = nil // 4 if let trackIndex = trackIndexForDownloadTask(downloadTask) { dispatch_async(dispatch_get_main_queue(), { self.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: trackIndex, inSection: 0)], withRowAnimation: .None) }) } } } |
Here’s the key steps from above:
- You extract the original request URL from the task and pass it to the provided
localFilePathForUrl(_:)
helper method.localFilePathForUrl(_:)
then generates a permanent local file path to save to by appending thelastPathComponent
of the URL (i.e. the file name and extension of the file) to the path of the app’s Documents directory. - Using
NSFileManager
, you move the downloaded file from its temporary file location to the desired destination file path by clearing out any item at that location before you start the copy task. - You look up the corresponding
Download
in your active downloads and remove it. - Finally, you look up the
Track
in your table view and reload the corresponding cell.
Build and run your project; pick any track and download it. When the download has finished, you should see the file path location printed to your console:
The Download button will also disappear, since the track is now on your device. Tap the track and you’ll hear it play in the presented MPMoviePlayerViewController
as shown below:
Monitoring Download Progress
Currently, you have no way to monitor the progress of the download. To improve the user experience, you’ll change your app to listen for download progress events and display the progress in the cells.
In SearchViewController.swift, find the extension that implements NSURLSessionDownloadDelegate
and add the following delegate method:
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { // 1 if let downloadUrl = downloadTask.originalRequest?.URL?.absoluteString, download = activeDownloads[downloadUrl] { // 2 download.progress = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite) // 3 let totalSize = NSByteCountFormatter.stringFromByteCount(totalBytesExpectedToWrite, countStyle: NSByteCountFormatterCountStyle.Binary) // 4 if let trackIndex = trackIndexForDownloadTask(downloadTask), let trackCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: trackIndex, inSection: 0)) as? TrackCell { dispatch_async(dispatch_get_main_queue(), { trackCell.progressView.progress = download.progress trackCell.progressLabel.text = String(format: "%.1f%% of %@", download.progress * 100, totalSize) }) } } } |
Looking through the delegate method step-by-step:
- Using the provided
downloadTask
, you extract the URL and use it to find theDownload
in your dictionary of active downloads. - The method also returns the total bytes written and the total bytes expected to be written. You calculate the progress as the ratio of the two values and save the result in the
Download
. You’ll use this value to update the progress view. NSByteCountFormatter
takes a byte value and generates a human-readable string showing the total download file size. You’ll use this string to show the size of the download alongside the percentage complete.- Finally, you find the cell responsible for displaying the
Track
, and update both its progress view and progress label with the values derived from the previous steps.
Next, you’ll configure the cells to properly display the progress view and status whenever a download is in progress.
Find the following line of code in tableView(_:cellForRowAtIndexPath:)
:
let downloaded = localFileExistsForTrack(track) |
Add the following code just before the line above:
var showDownloadControls = false if let download = activeDownloads[track.previewUrl!] { showDownloadControls = true cell.progressView.progress = download.progress cell.progressLabel.text = (download.isDownloading) ? "Downloading..." : "Paused" } cell.progressView.hidden = !showDownloadControls cell.progressLabel.hidden = !showDownloadControls |
For tracks with active downloads, you set showDownloadControls
to true
; otherwise, you set it to false
. You then display the progress views and labels, provided with the sample project, in accordance with the value of showDownloadControls
.
For paused downloads, display “Paused” for the status; otherwise, display “Downloading…”.
Finally, replace the following line:
cell.downloadButton.hidden = downloaded |
with the following code:
cell.downloadButton.hidden = downloaded || showDownloadControls |
Here, you tell the cell to also hide the Download button if its track is downloading.
Build and run your project; download any track and you should see the progress bar status update as the download progresses:
Hurray, you’ve made, erm, progress! :]
Pausing, Resuming and Cancelling Downloads
What if you need to pause a download, or cancel it altogether? In this section, you’ll implement the pause, resume and cancel features to give the user complete control over the download process.
You’ll start by allowing the user to cancel an active download.
Replace cancelDownload(_:)
with the following code:
func cancelDownload(track: Track) { if let urlString = track.previewUrl, download = activeDownloads[urlString] { download.downloadTask?.cancel() activeDownloads[urlString] = nil } } |
To cancel a download, you retrieve the download task from the corresponding Download
in the dictionary of active downloads and call cancel()
on it to cancel the task. You then remove it from the dictionary of active downloads.
Pausing a download is conceptually similar to cancelling; the difference is pausing cancels the download task, but also produces resume data, which contains enough information to resume the download at a later time, should the host server support that functionality.
Now, replace pauseDownload(_:)
with the following code:
func pauseDownload(track: Track) { if let urlString = track.previewUrl, download = activeDownloads[urlString] { if(download.isDownloading) { download.downloadTask?.cancelByProducingResumeData { data in if data != nil { download.resumeData = data } } download.isDownloading = false } } } |
The key difference here is the call to cancelByProducingResumeData(_:)
instead of cancel()
. You retrieve the resume data from the closure provided by cancelByProducingResumeData(:_)
and save it to the appropriate Download
for future resumption.
You also set the isDownloading
property of the Download
to false
to signify that the download is paused.
With the pause function completed, the next order of business is to allow the resumption of a paused download.
Replace resumeDownload(_:)
with the following code:
func resumeDownload(track: Track) { if let urlString = track.previewUrl, download = activeDownloads[urlString] { if let resumeData = download.resumeData { download.downloadTask = downloadsSession.downloadTaskWithResumeData(resumeData) download.downloadTask!.resume() download.isDownloading = true } else if let url = NSURL(string: download.url) { download.downloadTask = downloadsSession.downloadTaskWithURL(url) download.downloadTask!.resume() download.isDownloading = true } } } |
When the user resumes a download, you check the appropriate Download
for the presence of resume data. If found, you create a new download task by invoking downloadTaskWithResumeData(_:)
with the resume data and start the task by calling resume()
. If the resume data is absent for some reason, you create a new download task from scratch with the download URL anyway and start it.
In both cases, you set the isDownloading
flag of the Download
to true
to indicate the download has resumed.
There’s only one thing left to do for the three functions to work properly: you need to show or hide the Pause, Cancel and Resume buttons as appropriate.
Go to tableView(_:cellForRowAtIndexPath:)
and find the following line of code:
if let download = activeDownloads[track.previewUrl!] { |
Add the following lines to the end of the let
block above:
let title = (download.isDownloading) ? "Pause" : "Resume" cell.pauseButton.setTitle(title, forState: UIControlState.Normal) |
Since the pause and resume functions share the same button, the code above toggles the button between the two states as appropriate.
Next, add the following code to the end of tableView(_:cellForRowAtIndexPath:)
, just before the return
statement:
cell.pauseButton.hidden = !showDownloadControls cell.cancelButton.hidden = !showDownloadControls |
Here you simply show the buttons for a cell only if a download is active.
Build and run your project; download a few tracks concurrently and you’ll be able to pause, resume and cancel them at will:
Enabling Background Transfers
Your app is quite functional at this point, but there’s one major enhancement left to add: background transfers. In this mode, downloads continue even when your app is backgrounded or crashes for any reason.
But if your app isn’t running, how can this work? There’s a separate daemon that runs outside the app and manages background transfer tasks; it sends the appropriate delegate messages to the app as the download tasks run. In the event the app terminates during an active transfer, the tasks will continue to run unaffected in the background.
When a task completes, the daemon will relaunch the app in the background. The re-launched app will reconnect to the same session, receive the relevant completion delegate messages and perform any required actions such as persisting downloaded files to disk.
Still in SearchViewController.swift, in the initialization of downloadsSession
, find the following line of code:
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() |
…and replace it with the following line:
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("bgSessionConfiguration") |
Instead of using a default session configuration, you use a special background session configuration. Note that you also set a unique identifier for the session here to allow you to reference and “reconnect” to the same background session if needed.
Next, in viewDidLoad()
, add the following code:
_ = self.downloadsSession |
Calling the lazily-loaded downloadsSession
ensures the app creates exactly one background session upon initialization of SearchViewController
.
If a background task completes when app isn’t running, the app will be relaunched into the background. You’ll need to handle this event from your app delegate.
Switch to AppDelegate.swift and add the following code near the top of the class:
var backgroundSessionCompletionHandler: (() -> Void)? |
Next, add the following method to AppDelegate.swift
:
func application(application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: () -> Void) { backgroundSessionCompletionHandler = completionHandler } |
Here, you save the provided completionHandler
as a variable in your app delegate for later use.
application(_:handleEventsForBackgroundURLSession:)
wakes up the app to deal with the completed background task. You need to handle two things in this event:
- First, the app needs to reconnect to the appropriate background session with the identifier provided by the delegate method. But since you create and use one background session every time you instantiate
SearchViewController
, you’re already reconnected at this point! - Second, you’ll need to capture the completion handler provided by the delegate method. Invoking the completion handler causes the OS to snapshot your updated UI for display in the app switcher, as well as tells the OS that your app’s done working with all background activities for the current session.
But when should you invoke the completion handler? URLSessionDidFinishEventsForBackgroundURLSession(_:)
would be a good choice; it’s a NSURLSessionDelegate
method that fires when all tasks pertaining to the background session have finished.
Implement the following extension in SearchViewController.swift
:
extension SearchViewController: NSURLSessionDelegate { func URLSessionDidFinishEventsForBackgroundURLSession(session: NSURLSession) { if let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate { if let completionHandler = appDelegate.backgroundSessionCompletionHandler { appDelegate.backgroundSessionCompletionHandler = nil dispatch_async(dispatch_get_main_queue(), { completionHandler() }) } } } } |
The above code simply grabs the stored completion handler from the app delegate and invokes it on the main thread.
Build and run your app; start a few concurrent downloads and tap the Home button to background the app. Wait until you think the downloads have completed, then double-tap the Home button to reveal the app switcher.
The downloads should have finished and their new status reflected in the app screenshot. Open the app to confirm this:
You now have a fully functional music streaming app! Your move now, Apple Music! :]
Where to Go From Here?
You can download the complete project for this tutorial here.
Congratulations! You’re now well-equipped to handle most common networking requirements in your app. There are more details to NSURLSession
than would fit in this NSURLSession
tutorial, such as upload tasks and session configuration settings such as timeout values and caching policies.
To learn more about these features (and others!), check out the following resources:
- Apple’s documentation which contains comprehensive details on all the API methods.
- Our own iOS 7 By Tutorials book, with two full chapters dedicated to
NSURLSession
. You can also check out our earlier NSURLSession tutorial. - AlamoFire is a popular third-party iOS networking library; we covered the basics of it in our Beginning Alamofire tutorial.
I hope you found this tutorial useful. Feel free to join the discussion below!
The post NSURLSession Tutorial: Getting Started appeared first on Ray Wenderlich.