22

In the following code, the file downloads just fine. However none of the delegate methods seem to be called as I receive no output whatsoever. the progressView is not updated either. Any idea why?

import Foundation
import UIKit

class Podcast: PFQueryTableViewController, UINavigationControllerDelegate, MWFeedParserDelegate, UITableViewDataSource, NSURLSessionDelegate, NSURLSessionDownloadDelegate {

    func downloadEpisodeWithFeedItem(episodeURL: NSURL) {

    var request: NSURLRequest = NSURLRequest(URL: episodeURL)
    let config = NSURLSessionConfiguration.defaultSessionConfiguration()
    let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)

    var downloadTask = session.downloadTaskWithURL(episodeURL, completionHandler: { (url, response, error) -> Void in
        println("task completed")
        if (error != nil) {
            println(error.localizedDescription)
        } else {
            println("no error")
            println(response)
        }
    })
    downloadTask.resume()

}

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
    println("didResumeAtOffset")
}

    func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
          var downloadProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
    println(Float(downloadProgress))
    println("sup")

    epCell.progressView.progress = Float(downloadProgress)
}

     func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
    println(location)

}
}
user2747220
  • 863
  • 3
  • 12
  • 31

3 Answers3

35

From my testing, you have to choose whether you want to use a delegate or a completion handler - if you specify both, only the completion handler gets called. This code gave me running progress updates and the didFinishDownloadingToURL event:

func downloadEpisodeWithFeedItem(episodeURL: NSURL) {
    let request: NSURLRequest = NSURLRequest(URL: episodeURL)
    let config = NSURLSessionConfiguration.defaultSessionConfiguration()
    let session = NSURLSession(configuration: config, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
    
    let downloadTask = session.downloadTaskWithURL(episodeURL)
    downloadTask.resume()
}

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
    println("didResumeAtOffset: \(fileOffset)")
}

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
    var downloadProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
    println("downloadProgress: \(downloadProgress)")
}

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
    println("didFinishDownloadingToURL: \(location)")
    println(downloadTask)
}

From the NSURLSession documentation, here's the relevant section:

Like most networking APIs, the NSURLSession API is highly asynchronous. It returns data in one of two ways, depending on the methods you call:

  • To a completion handler block that returns data to your app when a transfer finishes successfully or with an error.
  • By calling methods on your custom delegate as the data is received.
  • By calling methods on your custom delegate when download to a file is complete.

So by design it returns data to either a completion handler block or a delegate. But as evinced here, not both.

Community
  • 1
  • 1
Nate Cook
  • 92,417
  • 32
  • 217
  • 178
  • I'm not sure thats correct at all, otherwise that would be horrendous API design. Furthermore take a look at AFNetworking, the AFURLSessionManager bases all the design on the delegate callbacks but you can still create requests with completion handlers... – Daniel Galasko Nov 02 '14 at 09:36
  • It may be horrendous API design, or perhaps a bug, since the behavior is undocumented. AFNetworking predates `NSURLSession` - it's based on `NSURLConnection`, which `NSURLSession` is meant to replace. In any case, persisting the session object doesn't fix the behavior. – Nate Cook Nov 02 '14 at 15:59
  • Thanks @nate! Cannot believe its designed like that. Guess the only option is to KVO onto a tasks state object to really keep control – Daniel Galasko Nov 03 '14 at 05:41
  • @DanielGalasko I guess I'm not following - with a delegate you can define a completion handler plus progress, interrupt, resume, etc. The completion handler that you specify in initialization (that overrides a delegate) is just a simpler way of setting it up if you *only* need to know when the download is complete. – Nate Cook Nov 03 '14 at 05:45
  • Definitely, the only issue is that if you design a session manager, you remain at the mercy of classes calling your task constructors not to use a completion handler, unless you use KVO on the task. Alternative is to provide your own completion handlers per task. – Daniel Galasko Nov 03 '14 at 05:51
  • 2
    its just an interesting API detail that the handler overrides the delegate:) – Daniel Galasko Nov 03 '14 at 05:52
  • 2
    "Interesting" is one way of putting it! Been puzzling over this for far too long. Why can't I use a completion handler and, for example, manage the caching behaviour through the delegate at the same time? Seems odd. – stephent Apr 02 '15 at 17:57
  • Thanks Nate! I had been trying for an hour to get a response from the delegate methods - but I too was using the completion handler. Thanks!! – adamteale Feb 12 '16 at 20:43
  • 1
    I can confirm that the completionHandler does indeed suppress the delegates. Also, don't forget to implement `URLSession:task:didCompleteWithError:` so that you receive any errors as well! (the completion handler provides you an `NSError *error` parameter). – EricWasTaken Feb 24 '16 at 02:49
20

Interestingly, Apple specifically explains this behavior in their NSURLSessionDataDelegate (but neither in the base delegate NSURLSessionTaskDelegate nor in NSURLSessionDownloadDelegate)

NOTE

An NSURLSession object need not have a delegate. If no delegate is assigned, when you create tasks in that session, you must provide a completion handler block to obtain the data.

Completion handler block are primarily intended as an alternative to using a custom delegate. If you create a task using a method that takes a completion handler block, the delegate methods for response and data delivery are not called.

Manfred Urban
  • 428
  • 5
  • 13
3

Swift 3

class ViewController: UIViewController {
    var urlLink: URL!
    var defaultSession: URLSession!
    var downloadTask: URLSessionDownloadTask!
}

// MARK: Button Pressed
    @IBAction func btnDownloadPressed(_ sender: UIButton) {
        let urlLink1 = URL.init(string: "https://github.com/VivekVithlani/QRCodeReader/archive/master.zip")
        startDownloading(url: urlLink!)
}
    @IBAction func btnResumePressed(_ sender: UIButton) {
    downloadTask.resume()
}

@IBAction func btnStopPressed(_ sender: UIButton) {
    downloadTask.cancel()
}

@IBAction func btnPausePressed(_ sender: UIButton) {
    downloadTask.suspend()
}

    func startDownloading (url:URL) {
        let backgroundSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "backgroundSession")
        defaultSession = Foundation.URLSession(configuration: backgroundSessionConfiguration, delegate: self, delegateQueue: OperationQueue.main)
        downloadProgress.setProgress(0.0, animated: false)
        downloadTask = defaultSession.downloadTask(with: urlLink)
        downloadTask.resume()
    }

// MARK:- URLSessionDownloadDelegate
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    print("File download succesfully")
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
    downloadProgress.setProgress(Float(totalBytesWritten)/Float(totalBytesExpectedToWrite), animated: true)
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    downloadTask = nil
    downloadProgress.setProgress(0.0, animated: true)
    if (error != nil) {
        print("didCompleteWithError \(error?.localizedDescription)")
    }
    else {
        print("The task finished successfully")
    }
}
Community
  • 1
  • 1
Vivek
  • 4,916
  • 35
  • 40