URLSession with background configuration doesn't works

In my iOS application I have problem in using URLSession with URLSessionConfiguration.background(withIdentifier:_). In my project i need to use background mode because my application must be able to fetch large files from server. For this case i had enabled "Background modes" - fetch and processing. Here is example of code which I use in my application (as on Your example in documentation https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background):

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var textView: UITextView!
    
    private var downloadRequest: URLSessionDownloadTask?
    
    private lazy var urlSession: URLSession = {
        let config = URLSessionConfiguration.background(withIdentifier: "backgroundLoadingSession")
        config.isDiscretionary = true
        config.sessionSendsLaunchEvents = true
        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
            self?.fetch()
        }
    }
    
    func fetch() {
        guard let url = URL(string: "https://images.idgesg.net/images/idge/imported/imageapi/2019/07/26/15/cloud_istock_harnnarong-100803439-large.jpg") else { return }
        downloadRequest = urlSession.downloadTask(with: url)
        downloadRequest?.resume()
    }
}

extension ViewController: URLSessionDownloadDelegate, URLSessionDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        DispatchQueue.main.async { [weak self] in
            self?.textView.text = downloadTask.response.debugDescription
        }
    }
}

When I'm running this code from Xcode on iPhone everything works fine - request sent, response received. But after application was terminated (or device was restarted) on next application launch (not by Xcode) anything doesn't happen - request not sent and as result response not received. I have no idea why it doesn't works and hope for Your help. I test this code on:

  • iPhone 6 (iOS 12.4.2), iPhone XsMax (iOS 13.3)
  • Xcode 11.2.1 (MacOS Catalina 10.15.1)
  • Xcode 10.3 (MacOS Mojave 10.14.6)

Also, here is my Info.plist file code:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleDevelopmentRegion</key>
  <string>$(DEVELOPMENT_LANGUAGE)</string>
  <key>CFBundleExecutable</key>
  <string>$(EXECUTABLE_NAME)</string>
  <key>CFBundleIdentifier</key>
  <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
  <key>CFBundleInfoDictionaryVersion</key>
  <string>6.0</string>
  <key>CFBundleName</key>
  <string>$(PRODUCT_NAME)</string>
  <key>CFBundlePackageType</key>
  <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
  <key>CFBundleShortVersionString</key>
  <string>1.0</string>
  <key>CFBundleVersion</key>
  <string>1</string>
  <key>LSRequiresIPhoneOS</key>
  <true/>
  <key>UIApplicationSceneManifest</key>
  <dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <false/>
  <key>UISceneConfigurations</key>
  <dict>
  <key>UIWindowSceneSessionRoleApplication</key>
  <array>
  <dict>
  <key>UISceneConfigurationName</key>
  <string>Default Configuration</string>
  <key>UISceneDelegateClassName</key>
  <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
  <key>UISceneStoryboardFile</key>
  <string>Main</string>
  </dict>
  </array>
  </dict>
  </dict>
  <key>UIBackgroundModes</key>
  <array>
  <string>fetch</string>
  <string>processing</string>
  </array>
  <key>UILaunchStoryboardName</key>
  <string>LaunchScreen</string>
  <key>UIMainStoryboardFile</key>
  <string>Main</string>
  <key>UIRequiredDeviceCapabilities</key>
  <array>
  <string>armv7</string>
  </array>
  <key>UISupportedInterfaceOrientations</key>
  <array>
  <string>UIInterfaceOrientationPortrait</string>
  <string>UIInterfaceOrientationLandscapeLeft</string>
  <string>UIInterfaceOrientationLandscapeRight</string>
  </array>
  <key>UISupportedInterfaceOrientations~ipad</key>
  <array>
  <string>UIInterfaceOrientationPortrait</string>
  <string>UIInterfaceOrientationPortraitUpsideDown</string>
  <string>UIInterfaceOrientationLandscapeLeft</string>
  <string>UIInterfaceOrientationLandscapeRight</string>
  </array>
</dict>
</plist>

You may running into a few different issues here; first, recreating the background session and asking the

session to resume a task that has already been completed in a short period of time could be causing rate

limiting issue. Another issue is that the URLSessionConfiguration.background(withIdentifier) is created

in a ViewController and you really want to move this off to an isolated object. As you move forward with

your project you will run into issues executing this background task inside of a view controller because

there is no guarantee that your view controller will be launched.


One approach to solve this is to create a custom object to delegate URLSessionConfiguration's that your

app is using in the context of the background or foreground. That way if you need a session at any point

you can call into this class and ensure it is isolated from your view controller.


An example of this might look something like:

// ViewController Example

class ViewController: UIViewController {
    
    @IBOutlet weak var textView: UITextView!


    override func viewDidLoad() {
        super.viewDidLoad()
        fetch()
    }


    func fetch() {
        let nsm = NetworkSessionManager(delegate: self)
        nsm.downloadWithFile(with: "yourImageURL")
    }
}


extension ViewController: NetworkDisplay {
    func updateUI(response: String) {
        textView.text = response
    }
}


// NetworkSessionManager Example


protocol NetworkDisplay: class {
    func updateUI(response: String)
}


class NetworkSessionManager: NSObject {
    
    private var urlSession: URLSession?
    private var downloadRequest: URLSessionDownloadTask?
    private weak var delegate: NetworkDisplay?
    
    init(withBackgroundSession: String, delegate: NetworkDisplay) {
        super.init()
        let config = URLSessionConfiguration.background(withIdentifier: withBackgroundSession)
        config.isDiscretionary = true
        config.sessionSendsLaunchEvents = true
        config.allowsCellularAccess = true
        self.delegate = delegate
        urlSession = URLSession(configuration: config, delegate: self, delegateQueue: .main)
    }
    
    init(delegate: NetworkDisplay) {
        super.init()
        let config = URLSessionConfiguration.default
        config.allowsCellularAccess = true
        self.delegate = delegate
        urlSession = URLSession(configuration: config, delegate: self, delegateQueue: .main)
    }
    
    func downloadWithFile(with url: String) {
        guard let url = URL(string: url),
            let unwrappedURLSession = urlSession else { return }
        downloadRequest = unwrappedURLSession.downloadTask(with: url)
        downloadRequest?.resume()
    }
    
}


extension NetworkSessionManager: URLSessionDownloadDelegate, URLSessionDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        delegate?.updateUI(response: downloadTask.response.debugDescription)
    }
    


    func urlSession(_ session: URLSession,
                downloadTask: URLSessionDownloadTask,
                didWriteData bytesWritten: Int64,
           totalBytesWritten: Int64,
           totalBytesExpectedToWrite: Int64) {
        let progress = (totalBytesWritten / totalBytesExpectedToWrite)
        // ...
    }


    func urlSession(_ session: URLSession,
                    task: URLSessionTask,
                    didCompleteWithError error: Error?) {
        if let errorStr = error?.localizedDescription {
            delegate?.updateUI(response: errorStr)
        }
    }

}


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Thanks for Your reply. I understand that URLSession in view controller is bad idea. Usually I implement singleton and provide all session information by NotificationCenter without delegates. But by given code example I want to in easy way show my problem. In my real project application should download file near 300MB size, not 2MB images. For that right i should use background mode and notifications, to give user possibility dismiss view controller which initiate loading process (application in foreground) and send application to background (go to home screen, launch another app, screen lock).


Your code example

let nsm = NetworkSessionManager(delegate: self)

use default session configuration

let config = URLSessionConfiguration.default

and in this case everything works fine except continue of downloading when application moved to background. In this case I receive error message "The operation couldn't be completed. Software caused connection abort".


Also, using Your code example, I changed default session manager initialization in view controller

let nsm = NetworkSessionManager(delegate: self)

to init(withBackgroundSession:)

let nsm = NetworkSessionManager(withBackgroundSession: "com.example.my.session", delegate: self)

which use background URLSessionConfiguration.

let config = URLSessionConfiguration.background(withIdentifier: withBackgroundSession)


Now, with background configuration downloading is resumed in background and i see response in text view. But I do next steps, after which problem occurs:

  1. Terminate application in app switcher
  2. Disconnect iPhone from Mac
  3. Launch application on iPhone - (app all time in foreground) usually in second I receive message "The operation couldn't be completed. No such file or directory."
  4. Restart iPhone
  5. Launch application on iPhone - (app all time in foreground) nothing not happened. Text view doesn't changed, response never been received. Reason - request wasn't sent. I know it because I can see all requests in our local network using special proxy.

Just try repeat those steps. Hope this explanation will be more useful and help to solve this problem.

By changing the NetworkSessionManager to use a background session in the ViewController this gets you back into the original situation I wanted you to avoid. Use the background initializer when your task is run in the background with an API like BGTaskScheduler. When you execute a task from the foreground, use the default initializer that only passes in the delegate. If you start a long running download in the foreground you should really try and finish it in the foreground, but for situations where the user absolutely has to enter the background there are a couple of options; First, for a download you could try and resume the task. This requires some server work, as seen here, but if this is setup you could attempt to resume the download from where it left off like this:


class NetworkSessionManager: NSObject {
    
    // ...
    private var resumeData: Data?
    
    
    // ...
    
    func attemptToResume() {
        if let unwrappedResumeData = resumeData {
            downloadRequest = urlSession?.downloadTask(withResumeData: unwrappedResumeData)
            downloadRequest?.resume()
            resumeData = nil
        }
    }
    
}




extension NetworkSessionManager: URLSessionDownloadDelegate, URLSessionDelegate {
    
    // ...


    func urlSession(_ session: URLSession,
                    task: URLSessionTask,
                    didCompleteWithError error: Error?) {


        if let unwrappedError = error {
  
            let userInfo = (unwrappedError as NSError).userInfo
            if let resumeData = userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
                self.resumeData = resumeData
                delegate?.updateUI(response: "Error downloading, resuming download")
                attemptToResume()
            } else {
                delegate?.updateUI(response: unwrappedError.localizedDescription)
            }
        }
    }


}



Second, if your task is not able to be resumed, you could build a mechanism to look for failed downloads and use a background task to attempt to clean these up. For this you could use the BGTaskScheduler.


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Hi Matt Eaton,

I know that this is an older post, but hoped you would be willing to update your NetworkSessionManager example for swiftui?

The
Code Block
protocol NetworkDisplay: class {
func updateUI(response: String)
}
Is the real sticky point.

For example if we have a view like:
Code Block
struct ReadingsRow: View {
@ObservedObject var viewModel: ListingViewModel
@EnvironmentObject var env: GlobalEnvironment
var reading: Reading
@State var progressValue: Float = 0.25
var body: some View {
VStack {
HStack{
Text("\(reading.title!)")
.font(.custom("Helvetica Neue", size: 16))
.fontWeight(.semibold)
Button(action: {
let videoToDownload = Download(reading: self.reading)
self.viewModel.downloadVimeoVideo(downloadObj: videoToDownload)
}) {
Image(systemName: "arrow.down.left.video")
}
}
HStack {
VStack {
ProgressBar(value: $progressValue).frame(height: 5)
Spacer()
}.padding()
}
}.onAppear() {
print("isOnline", self.env.getOnlineStatus())
}
}
}


Here is the class method that self.viewModel.dowladVimeoVideo() uses:
Code Block
class BackgroundDownloadTasks{
/// Get local file path: download task stores tune here; AV player plays it.
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
func downloadVideoInBackground(downloadVideo: Download) {
let bgHeaders = mmHeaders(
ContentType: "application/video",
HttpMethod: "POST",
Authorization: "<--Bearer Token Here-->"
)
let ST = SessionTasks(url: downloadVideo.url)
let NSM = NetworkSessionManager(withBackgroundSession: "com.somethingunique", delegate: <#NetworkDisplay#>)
let bkgRequest = ST.getRequest(mmHeaders: bgHeaders)
NSM.downloadWithFile(with: downloadVideo.url.absoluteString, from: bkgRequest)
}
}


I realize that the $progressValue state variable will not work in my example as each ReadingRow has to have a unique progressBar value. However, ignoring this, how would we make use of updateUI() to set the $progressValue in the ProgressBar(value: progressValue).frame(height: 5)?


Best Regards,
Steve Browning


URLSession with background configuration doesn't works
 
 
Q