Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

NSURLSession Tutorial: Getting Started

$
0
0
Learn how to implement HTTP data requests and downloads with the NSURLSession API!

Learn how to make HTTP data requests and implement file downloads with NSURLSession!

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:

Half Tunes Starter Project

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 Tutorial

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.

NSURLSession Tutorial

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:

  1. You create a NSURLSession and initialize it with a default session configuration.
  2. 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:

  1. 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.
  2. You enable the network activity indicator on the status bar to indicate to the user that a network process is running.
  3. 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.
  4. Next you construct a NSURL by appending the escaped search string as a GET parameter to the iTunes Search API base url.
  5. From the session you created, you initialize a NSURLSessionDataTask to handle the HTTP GET request. The constructor of NSURLSessionDataTask takes in the NSURL that you constructed along with a completion handler to be called when the data task is completed.
  6. Upon receiving a callback that the task completed, you hide the activity indicator and invoke the UI update in the main thread.
  7. If the HTTP request is successful, you call updateSearchResults(_:), which parses the response NSData into Tracks and updates the table view.
  8. 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:

HalfTunes Search

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 and make it look like this:

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 :

  1. You first initialize a Download with the preview URL of the track.
  2. Using your new session object, you create a NSURLSessionDownloadTask with the preview URL, and set it to the downloadTask property of the Download.
  3. You start the download task by calling resume() on it.
  4. You indicate that the download is in progress.
  5. Finally, you map the download URL to its Download in the activeDownloads 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:

  1. 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 the lastPathComponent of the URL (i.e. the file name and extension of the file) to the path of the app’s Documents directory.
  2. 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.
  3. You look up the corresponding Download in your active downloads and remove it.
  4. 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:

Screen Shot 2015-08-16 at 8.03.30 pm

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:

Simulator Screen Shot 17 Aug 2015 1.45.28 am

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:

  1. Using the provided downloadTask, you extract the URL and use it to find the Download in your dictionary of active downloads.
  2. 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.
  3. 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.
  4. 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:

Screen Shot 2015-08-17 at 11.02.03 pm

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.

Note: You can only resume a download under certain conditions. For instance, the resource must not have changed since you first requested it. For a full list of conditions, check out the Apple documentation here.

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:

Simulator Screen Shot 18 Aug 2015 10.14.38 pm

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.

Note: If you terminate the app by force-quiting from the app switcher, the system will cancel all of the session’s background transfers and won’t attempt to relaunch the app.

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:

Simulator Screen Shot 19 Aug 2015 1.06.24 am

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:

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.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>