Learn NSURLSession using Swift Part 2 — Background download

Based on previous article (Learn NSURLSession using Swift Part 1 — HTTP/HTTPS GET), we already understand what NSURLSession can do for us. Now, let’s find out the most important behavior — Background Downloads.

Reason

Users apparently loves to download some useful stuff on their cell-phone, like images, music, etc. Using Apps which provides download behavior is joyful, but it’s better if we can left the App to finish its job in background and use other Apps in foreground at same time, then back to see the download results after a while.

Source Code

Since we are going to present download process based on the App lifecycle, this time I will demo using entire Swift project, you can download the source code here: https://github.com/samwang0723/URLSessionBackgroundDownload

Session Identifier and create Session & Task

As previously mentioned, there are three kinds of types of NSURLSession, we are going to use download sessions with background configuration to enable background download in this tutorial. Refer to (backgroundSessionConfigurationWithIdentifier(_: ), The unique identifier for the configuration object. This parameter must not be nil or an empty string. In below code sample I created a struct to store our identifier as static, because it will be use in many place.

struct SessionProperties {
static let identifier:String! =
“url_session_background_download”
}

Then I’m gonna create the background configuration

var configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(SessionProperties.identifier)

And create background session

var backgroundSession = NSURLSession(configuration: configuration, delegate: self.delegate, delegateQueue: nil)

Custom Delegate

Notice that we need to have a customized delegate(self.delegate), implement NSURLSessionDelegate, NSURLSessionDownloadDelegate, I made this delegate with singleton(called with sharedInstance) and can allow access by different activity without duplicate many copies.

typealias CompleteHandlerBlock = () -> ()
class DownloadSessionDelegate : NSObject, NSURLSessionDelegate, NSURLSessionDownloadDelegate {

var handlerQueue: [String : CompleteHandlerBlock]!
class var sharedInstance: DownloadSessionDelegate {
struct Static {
static var instance : DownloadSessionDelegate?
static var token : dispatch_once_t = 0
}

dispatch_once(&Static.token) {
Static.instance = DownloadSessionDelegate()
Static.instance!.handlerQueue = [String :
CompleteHandlerBlock]()
}

return Static.instance!
}

//MARK: session delegate
func URLSession(session: NSURLSession, didBecomeInvalidWithError
error: NSError?) {
println(“session error: \(error?.localizedDescription).”)
}

func URLSession(session: NSURLSession, didReceiveChallenge
challenge: NSURLAuthenticationChallenge,
completionHandler:
(NSURLSessionAuthChallengeDisposition,
NSURLCredential!) -> Void) {
completionHandler(
NSURLSessionAuthChallengeDisposition.UseCredential,
NSURLCredential(forTrust:
challenge.protectionSpace.serverTrust))
}

func URLSession(session: NSURLSession,
downloadTask:NSURLSessionDownloadTask,
didFinishDownloadingToURL location: NSURL) {
println(“session \(session) has
finished the download task \(downloadTask) of URL
\(location).”)
}

func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
println(“session \(session) download task \(downloadTask)
wrote an additional \(bytesWritten) bytes (total \
(totalBytesWritten) bytes) out of an expected \
(totalBytesExpectedToWrite) bytes.”)
}
   func URLSessionDidFinishEventsForBackgroundURLSession(session: 
NSURLSession) {
println(“background session \(session) finished events.”)

if !session.configuration.identifier.isEmpty {
callCompletionHandlerForSession(
session.configuration.identifier)
}
}

//MARK: completion handler
func addCompletionHandler(handler: CompleteHandlerBlock,
identifier: String) {
handlerQueue[identifier] = handler
}

func callCompletionHandlerForSession(identifier: String!) {
var handler : CompleteHandlerBlock =
handlerQueue[identifier]!
handlerQueue!.removeValueForKey(identifier)
handler()
}
}

In URLSession(_:downloadTask:didWriteData:totalBytesWritten:
totalBytesExpectedToWrite
, we can receive the callback from current download process and tell the written bytes, this delegate method can use to calculate the download progress percentage by “var progress = totalBytesWritten / totalBytesExpectedToWrite”.

In URLSession(_:downloadTask:didFinishDownloadingToURL means the download has finished. URLSessionDidFinishEventsForBackground
URLSession(_
: ) Tells the delegate that all messages enqueued for a background session have been delivered.

AppDelegate will handle the resume while download completed

The last two functions addCompletionHandler, callCompletionHandlerForSession used for handle complete notification from AppDelegate, when user put your App into background, the download will still proceed and automatically relaunched your App when a download completes by calling application:handleEventsForBackgroundURLSession:
completionHandler:
in AppDelegate as below.

//MARK: background session handling
func application(application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: () -> Void) {
println(“— handleEventsForBackgroundURLSession —”)
var backgroundConfiguration = NSURLSessionConfiguration.
backgroundSessionConfigurationWithIdentifier(identifier)
var backgroundSession = NSURLSession(configuration:
backgroundConfiguration, delegate: self.delegate,
delegateQueue: nil)
println(“Rejoining session \(backgroundSession)”)

self.delegate.addCompletionHandler(completionHandler,
identifier: identifier)
}

When background download complete and trigger AppDelegate, we retrieve back the session use identifier, and put the completionHandler into queue, while our custom delegate receive the URLSessionDidFinishEventsFor
BackgroundURLSession
, we can get the Closure and execute it to complete the whole process, it is really important because calling completion handler lets the system know that your app’s user interface is updated and a new snapshot can be taken.

Now implement the download task, you can simply create a download task with downloadTaskWithRequest(_: ), and perform it with resume()

var url = NSURLRequest(URL: NSURL(string: data[4])!)
var downloadTask = backgroundSession.downloadTaskWithRequest(url)
downloadTask.resume()

Project Setup for Background Mode

Most of the documents said the code above should be able to let developers to enable background download, but actually I can’t, the reason is that I didn’t enable Background Modes in the Project Capabilities.

Right now execute these code on your iPhone, and you can see what’s happen if put App to background while downloading(press Home key), and waiting to be triggered when download complete.

session <__NSURLBackgroundSession: 0x170128480> download task <__NSCFBackgroundDownloadTask: 0x125d140a0>{ taskIdentifier: 1 } wrote an additional 65536 bytes (total 234795690 bytes) out of an expected 262144000 bytes.
session <__NSURLBackgroundSession: 0x170128480> download task <__NSCFBackgroundDownloadTask: 0x125d140a0>{ taskIdentifier: 1 } wrote an additional 65536 bytes (total 234867558 bytes) out of an expected 262144000 bytes.
session <__NSURLBackgroundSession: 0x170128480> download task <__NSCFBackgroundDownloadTask: 0x125d140a0>{ taskIdentifier: 1 } wrote an additional 65536 bytes (total 234938070 bytes) out of an expected 262144000 bytes.
session <__NSURLBackgroundSession: 0x170128480> download task <__NSCFBackgroundDownloadTask: 0x125d140a0>{ taskIdentifier: 1 } wrote an additional 65536 bytes (total 235015362 bytes) out of an expected 262144000 bytes.
— handleEventsForBackgroundURLSession —
2014–11–04 16:51:13.070 URLSessionBackgroundDownload[13461:2121612] A background URLSession with identifier url_session_background_download already exists!
Rejoining session <__NSURLBackgroundSession: 0x170128480>
session <__NSURLBackgroundSession: 0x170128480> has finished the download task <__NSCFBackgroundDownloadTask: 0x125d140a0>{ taskIdentifier: 1 } of URL file:///private/var/mobile/Containers/Data/Application/23F2A8ED-14BC-485E-B747-C651B000CDA7/Library/Caches/com.apple.nsurlsessiond/Downloads/org.samwang.URLSessionBackgroundDownload/CFNetworkDownload_7KoBTL.tmp.
background session <__NSURLBackgroundSession: 0x170128480> finished events.

And of course, please move the temporary file into Document or some place in didFinishDownloadingToURL delegate method.

Next chapter we can discuss about the upload related topic of NSURLSession, hope you will like it.