Search code examples
iosswiftswift3nsurlsessionnsurlsessiondownloadtask

Cannot get data from URLSessionTaskDelegate


I am trying to add background fetch capability to my app. Currently, the delegate function urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) is called after the network call is complete, but the URL in the cache directory does not exist:

class DownloadManager: NSObject, URLSessionTaskDelegate, URLSessionDownloadDelegate {

static var shared = DownloadManager()

var session : URLSession {
    get {
        let config = URLSessionConfiguration.background(withIdentifier: "my_Identifier")
        return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
    }
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    print(location.absoluteString)
    do {
        let myData = try Data(contentsOf: location)
    } catch let error {
        print(error.localizedDescription)
      // The The file “file_id.tmp” couldn’t be opened because there is no such file.
    }
}

public func fetch() {
    guard let url = URL(string: "#{myURL}") else {
        return
    }

    let task = session.downloadTask(with: url)
    task.resume()
  }
}

And in my App Delegate:

func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    print("Executing background fetch")
    DownloadManager.shared.fetch()
    completionHandler(.newData)
}

What am I missing?


Solution

  • Try using this:

    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var progressView: UIProgressView!
    
        override func viewDidLoad() {
            let _ = DownloadManager.shared.activate()
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            DownloadManager.shared.onProgress = { (progress) in
                OperationQueue.main.addOperation {
                    self.progressView.progress = progress //assign progress value
                }
            }
        }
    
        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
            DownloadManager.shared.onProgress = nil
        }
    
        @IBAction func startDownload(_ sender: Any) {
            let url = URL(string: "YourFileURL")!
            DownloadManager.shared.download(url)
        }
    
    }
    


    Replace your DownloadManager:

        import Foundation
    
    class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
    
        static var shared = DownloadManager()
        var url : URL?
        typealias ProgressHandler = (Float) -> ()
    
        var onProgress : ProgressHandler? {
            didSet {
                if onProgress != nil {
                    let _ = activate()
                }
            }
        }
    
        override private init() {
            super.init()
        }
    
        func activate() -> URLSession {
            let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
    
            // Warning: If an URLSession still exists from a previous download, it doesn't create a new URLSession object but returns the existing one with the old delegate object attached!
            return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
        }
    
        private func calculateProgress(session : URLSession, completionHandler : @escaping (Float) -> ()) {
            session.getTasksWithCompletionHandler { (tasks, uploads, downloads) in
                let progress = downloads.map({ (task) -> Float in
                    if task.countOfBytesExpectedToReceive > 0 {
                        return Float(task.countOfBytesReceived) / Float(task.countOfBytesExpectedToReceive)
                    } else {
                        return 0.0
                    }
                })
                completionHandler(progress.reduce(0.0, +))
            }
        }
    
        func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
    
            if totalBytesExpectedToWrite > 0 {
                if let onProgress = onProgress {
                    calculateProgress(session: session, completionHandler: onProgress)
                }
                let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
                debugPrint("Progress \(downloadTask) \(progress)")
    
            }
        }
    
        func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
            debugPrint("Download finished: \(location)")
    //        try? FileManager.default.removeItem(at: location)
            //copy downloaded data to your documents directory with same names as source file
            let documentsUrl =  FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
            let destinationUrl = documentsUrl!.appendingPathComponent(url!.lastPathComponent)
            let dataFromURL = try? Data(contentsOf: location)
            try? dataFromURL?.write(to: destinationUrl, options: [.atomic])
            print(destinationUrl)
            //now it is time to do what is needed to be done after the download
        }
    
        func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
            debugPrint("Task completed: \(task), error: \(String(describing: error))")
        }
    
    
        func download(_ url: URL)
        {
            self.url = url
    
            //download identifier can be customized. I used the "ulr.absoluteString"
            let task = DownloadManager.shared.activate().downloadTask(with: url)
            task.resume()
    
        }
    }
    


    And in my App Delegate:

    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
            debugPrint("handleEventsForBackgroundURLSession: \(identifier)")
            completionHandler()
        }
    

    Reference: Tutorial by ralfebert