3

I have FirstViewController with button to move in SecondTableViewController. In SecondTableViewController I have cell and if I click on cell downloading starts.

Problem: If I move in SecondTableViewController from FirstViewController to start downloading and return in FirstViewController and after move in SecondTableViewController I get this:

A background URLSession with identifier com.example.DownloadTaskExample.background already exists!

And I can not download my files. How to fix it?

my code in SecondTableViewController:

var backgroundSession: URLSession!
var index = 0

override func viewDidLoad() {
    super.viewDidLoad()
    let sessionConfig = URLSessionConfiguration.background(withIdentifier: "com.example.DownloadTaskExample.background")
        backgroundSession = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: OperationQueue())
}

code for download files:

let url = URL(string: "link")!
let downloadTaskLocal = ViewController.backgroundSession.downloadTask(with: url)
downloadTaskLocal.resume()

New code:

class Networking {
    static let shared = Networking()
    var backgroundSession = URLSession(configuration: URLSessionConfiguration.background(withIdentifier: "com.example.DownloadTaskExample.background"), delegate: URLSession() as? URLSessionDelegate, delegateQueue: OperationQueue())
}

class ViewController: UITableViewController, URLSessionDownloadDelegate {

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

        let url = URL(string: "link")!
        let downloadTaskLocal = Networking.shared.backgroundSession.downloadTask(with: url)
        downloadTaskLocal.resume()
    }
}

UPD

class BackgroundSession: NSObject {

    static let shared = BackgroundSession()

    static let identifier = "com.example.DownloadTaskExample.background"

    var session: URLSession!

    private override init() {
        super.init()

        let configuration = URLSessionConfiguration.background(withIdentifier: BackgroundSession.identifier)
        session = URLSession(configuration: configuration, delegate: self as? URLSessionDelegate, delegateQueue: OperationQueue())
    }


}

class ViewController: UITableViewController, URLSessionDownloadDelegate{

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let url = URL(string: "http:link\(indexPath.row + 1)")!
        let downloadTaskLocal = BackgroundSession.shared.session.downloadTask(with: url)
        downloadTaskLocal.resume()
}
}

2 Answers2

4

If you're really using background session, you should make sure to instantiate it only once. You need to make this single URLSession instance available to not only multiple instances of the second view controller, but you also need to have your app delegate be able to reference it (e.g. handleEventsForBackgroundURLSession has to save the completion handler, to be called but session delegate's urlSessionDidFinishEvents(forBackgroundURLSession:)).

One approach is to have the app delegate instantiate it and then pass it along. Even easier, you can use singleton pattern (as shown in https://stackoverflow.com/a/44140059/1271826).

The trick is that in decoupling this background session from any particular instance of the second view controller, how do you want to inform this second view controller of events from your background session. You might want to use NotificationCenter. Or you could give your background session some closure properties that the second view controller could set (and reset for every new instance). It's hard to say precisely without knowing what that second view controller is doing.

But the key is to make sure you have only one instance of the background session during the lifetime of the app.


By the way, two other problems. Consider:

session = URLSession(configuration: configuration, delegate: self as? URLSessionDelegate, delegateQueue: OperationQueue())
  1. You never conformed to URLSessionDelegate (and you hid this problem with as? conditional type casting). Remove the as? URLSessionDelegate. And obviously, also add conformance with an extension:

    extension BackgroundSession: URLSessionDelegate { ... }
    

    Obviously, inside that, implement whatever URLSessionDelegate methods you need, if any.

    You probably also want to conform to URLSessionTaskDelegate and URLSessionDownloadDelegate in their own extensions, too, if you want to implement any of those methods, too.

  2. We generally want URLSession to use a serial queue. As the documentation says:

    The queue should be a serial queue, in order to ensure the correct ordering of callbacks. If nil, the session creates a serial operation queue for performing all delegate method calls and completion handler calls.

    So, easiest, just pass nil as that third parameter, and a serial queue will be created for you. But by passing OperationQueue(), you’re supplying a concurrent queue, when you really want a serial one.

Thus that line of code should be replaced with:

session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I clicked to link: https://stackoverflow.com/a/44140059/1271826 but I can not apply this to my code. I added new code in question in **UPD**. Could you help me? My file not downloading. What is wrong? –  Aug 21 '17 at 16:00
  • Is that your whole `BackgroundSession` implementation? You have to implement the `URLSessionDownloadDelegate` method [`didFinishDownloadingTo`](https://developer.apple.com/documentation/foundation/urlsessiondownloaddelegate/1411575-urlsession) in `BackgroundSession`. Also, if doing background sessions, you need to implement `handleEventsForBackgroundSession` in your app delegate (saving the completion handler) as well as `urlSessionDidFinishEvents(forBackgroundURLSession:)` (calling the completion handler) in `BackgroundSession`, as outlined in that other answer. – Rob Aug 21 '17 at 16:10
  • I need to move the `didWriteData bytesWritten` if I have this in ViewController in BackgroundSession? –  Aug 21 '17 at 16:34
  • Yep. And then give `BackgroundSession` some way to inform the view controller (e.g. a closure, your own protocol, or a notification). – Rob Aug 21 '17 at 18:54
  • I asked a new question that is related to this. Could you check? https://stackoverflow.com/questions/45819015/show-progress-in-tableview-cell –  Aug 22 '17 at 14:50
0

First of all, you should move all network related code into a separate class and shouldn't put it inside a ViewController class. backgroundSession also shouldn't be an implicitly unwrapped optional.

In the meantime, this is how you can fix your current code (move the declaration of backgroundSession to the class level):

var backgroundSession = URLSession(configuration: URLSessionConfiguration.background(withIdentifier: "com.example.DownloadTaskExample.background"), delegate: self, delegateQueue: OperationQueue())

var index = 0

override func viewDidLoad() {
    super.viewDidLoad()
    SecondTableViewController.backgroundSession //do something with the session, it will only be evaluated when you are trying to access it
}

In the long term, you should create a class for network related code and it should have a singleton instance.

class Networking {
    static let shared = Networking()
    var backgroundSession = URLSession(configuration: URLSessionConfiguration.background(withIdentifier: "com.example.DownloadTaskExample.background"), delegate: self, delegateQueue: OperationQueue())
}

This way you can access the singleton instance of the Networking class by writing Networking.shared and access the backgroundSession by Networking.shared.backgroundSession.

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • I add your code but file not downloading. I add new code for download file in question. Check, please. –  Aug 20 '17 at 19:14
  • I agree with your broader prescription (completely pull it out of the view controllers), but the problem with the `static` implementation is that the delegate `self` will refer to the class, not any particular instance of that class. – Rob Aug 20 '17 at 20:05
  • @Artem check my updated answer, I think the problem was with the delegate – Dávid Pásztor Aug 20 '17 at 21:44
  • 1
    @Rob thanks for the feedback, I didn't think about the delegate. Updated my answer with an intermediate solution where the `backgroundSession` is an instance property of the `ViewController` class. And for the long term, included a skeleton for the `Networking` class with a singleton instance of the class, that should solve the delegate issue as well. – Dávid Pásztor Aug 20 '17 at 21:46
  • @DávidPásztor I added a new code. I use this code, but the file does not downloading. What's wrong? –  Aug 21 '17 at 06:39
  • Did you implement the necessary delegate methods to handle the background session? I only included the code necessary to solve the issue you mentioned in your question (trying to recreate the session several times), I didn't provide a full implementation of a background `URLSession`. – Dávid Pásztor Aug 21 '17 at 08:57
  • @DávidPásztor I use only your code. What else I should do? –  Aug 21 '17 at 10:29
  • Have a look at [this](https://stackoverflow.com/a/44140059/1271826) answer, it tells you what you need to do to make background sessions work. – Dávid Pásztor Aug 21 '17 at 10:30
  • In my code I create link like this: let url = URL(string: "link\(indexPath.row)")! But if I use background session in Networking class I should create link in this class too? –  Aug 21 '17 at 15:36
  • No, you can start the backgroundSession's downloadTask with the link created in the ViewController class as well. You shouldn't create the link in the Networking class. – Dávid Pásztor Aug 21 '17 at 15:38
  • As an aside, I would not suggest using `OperationQueue()`. That creates a concurrent queue which causes race conditions with the various delegate methods. It's better to just leave that `nil`, for which the OS will create its own serial operation queue for the delegate methods. Or, if you really want to create your own operation queue, Apple advises that you make it a serial queue (set `maxConcurrentOperationCount` to 1). – Rob Aug 21 '17 at 16:04