How to handle URLSessiosn UploadTask ResumeData when delegate method got fired? - swift

I was implementing an upload task using URLSession Uploadtask with the below code:
lazy var urlSession = URLSession(
configuration: .background(withIdentifier: "com.test.xxxxx"),
delegate: self,
delegateQueue: .main
)
var uploadTask = URLSessionUploadTask()
/// Calling uploadtask using 'fileURL' of the asset
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "PUT"
uploadTask = urlSession.uploadTask(with: request, fromFile: fileURL)
uploadTask.resume()
And uploading works as expected, my concern is if I want to use resume data whenever user removes the app from multitask window or any error happens in between uploading a file, how can i achieve it using below delegate method, this delegate method is firing for me, but we don't have any methods to use resume data like func downloadTask(withResumeData resumeData: Data) -> URLSessionDownloadTask for upload task or is it not possible for upload task, please guide me on this. Thank you.
func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
// Here comes when background upload failed with errors
// Such as app closed from the tray ,switched off ,crash occured, etc.
// Then, handle with resumeData
os_log("Download error: %#", type: .error, String(describing: error))
} else {
// Here comes when background upload completed with no error
os_log("Task finished: %#", type: .info, task)
}
}
Edit: I can't see anything related to resume data for upload task in Apple doc also.

Related

URLSessionDelegate functions not called for background upload on watchOS

I'm using a Series 6 emulator on watchOS7 and I'm trying to upload some JSON data to a remote server using an URLSession background task. However the delegate functions are not being called so I cannot clean up any local data from the upload and handle any errors. I got the original idea from my implementation from this WWDC video WWDC Video. I've looked at many posts on the Internet but nothing I've tried seems to work. Here is my code:
UploadSession class
class UploadSession: NSObject, Identifiable, URLSessionDelegate {
var backgroundTasks = [WKURLSessionRefreshBackgroundTask]()
private lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "my.app.watchextension")
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
func enqueueBackgroundTask(idToken: String, uploadData: Data, url: URL) throws {
//build the JSON object we want to send in this post request
let tempDir = FileManager.default.temporaryDirectory
let localURL = tempDir.appendingPathComponent("throwaway")
try? uploadData.write(to: localURL)
//set up the URLRequest
var request = URLRequest(url: url)
request.httpMethod = K.Upload.httpPost
request.setValue(K.Upload.jsonContent, forHTTPHeaderField: K.Upload.contentType)
request.setValue("\(K.Upload.bearer)\(idToken)", forHTTPHeaderField: K.Upload.authorization)
request.timeoutInterval = K.Upload.httpUploadTimeout
//keep a reference to this class
BackgroundURLSessions.shared.sessions["my.app.watchextension"] = self
//create the upload task and kick it off
let task = urlSession.uploadTask(with: request, fromFile: localURL)
task.earliestBeginDate = Date().advanced(by: 120)//when setting this to zero the upload runs straight away.
task.resume()
}
func addBackgroundRefreshTask(_ task: WKURLSessionRefreshBackgroundTask) {
backgroundTasks.append(task)
}
//gets called when the task background task completes
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
//PROBLEM. This delegate method is never called
if let sessionId = session.configuration.identifier {
//TODO delete any local data copies
//set the session to nil so the system doesn't try to execute it again
BackgroundURLSessions.shared.sessions[sessionId] = nil
}
for task in backgroundTasks {
task.setTaskCompletedWithSnapshot(false)
}
}
//gets called if the background task throws an error
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
//PROBLEM. This delegate method is never called
}
}
BackgroundURLSessions class
class BackgroundURLSessions: NSObject {
static let shared: BackgroundURLSessions = BackgroundURLSessions()
var sessions = [String: UploadSession]()
override private init() {
super.init()
}
}
Extension Delegate class
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for task in backgroundTasks {
// Use a switch statement to check the task type
switch task {
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once you’re done.
if let session = BackgroundURLSessions.shared.sessions[urlSessionTask.sessionIdentifier] {
session.addBackgroundRefreshTask(urlSessionTask)
} else {
urlSessionTask.setTaskCompletedWithSnapshot(false)
}
default:
// make sure to complete unhandled task types
task.setTaskCompletedWithSnapshot(false)
}
}
}
Any help really appreciated as I'm really struggling to get this work and it's a vital part of the application. Thanks.

URLSession omits delegate's redirection management method

I am sending a DataTask to some website and in URL I have redirection to localhost (https://...&redirect_uri=http://localhost...). Overall in that call I am getting about 5 redirections, where localhost is probably third and after that redirection localhost keeps in its URL important string that I want to take, so I decided to prevent redirections, when URL will start with localhost and its further specific URL:
extension MyClass: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
if response.url?.absoluteString.hasPrefix("http://localhost/bar?code=") ?? false {
completionHandler(nil)
}
completionHandler(request)
}
and I set my URLSession object's delegate as that class:
private var session: URLSession { URLSession(configuration: .default, delegate: self, delegateQueue: nil) } // `self` is MyClass
After sending request mentioned in the beginning of the question Xcode throws that error:
Task <UIID>.<1> finished with error [-1004] Error Domain=NSURLErrorDomain Code=-1004 "Could not connect to the server." UserInfo={NSUnderlyingError=0x600000cc46f0 {Error Domain=kCFErrorDomainCFNetwork Code=-1004 "(null)" UserInfo={_kCFStreamErrorCodeKey=61, _kCFStreamErrorDomainKey=1}}, NSErrorFailingURLStringKey=http://localhost/bar?code=[code], NSErrorFailingURLKey=http://localhost/bar?code=[code], _kCFStreamErrorDomainKey=1, _kCFStreamErrorCodeKey=61, NSLocalizedDescription=Could not connect to the server.}
As You see, that delegate method isn't called (I've additionally checked that with breakpoints). I found that some other methods of that delegate doesn't fire when I am using closure on dataTask(with:), but it doesn't apply on redirection handling method (I've checked that, I have deleted closures and it still isn't called).
Additional info:
Request is created like here:
let url = URL(string: https://...)
var request = URLRequest(url: url)
request.addValue([someValue], forHTTPHeaderField: "User-Agent")
And this is my task:
let task = session.dataTask(with: request) // Request created like above
task.resume()
And will Combine methods (.dataTaskPubliser()) apply to that delegate?
I found that's probably a bug (reported). Combine's publisher of URLSessionDataTask just doesn't call some methods of the delegate. Switched for now to classic Foundation's implementation.

How to go back to DispatchQueue.main from URLSession.shared.dataTask (macOS framework)

I'm building a macOS framework and at some point, I need to make a request to some API
When I got the response I want to update the UI. I'm using URLSession.shared.dataTask to make the call and as I know the call is made in the background thread
For some reason when I try to go back to the main thread nothing happens
I'm using a virtual machine to run my framework
Any help?
Thanks
Here how I doing the request:
URLSession.shared.dataTask(with: request) { data, response, error in
if error != nil {
DispatchQueue.main.async {
//Display error message on the UI
//This never happens
//Never go back to the main thread
//Framework stop working
}
}
}.resume()
Are you sure that your task is called?
let dataTask = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
....
DispatchQueue.main.async {
completion(nil, nil)
}
}
dataTask.resume() // You should add this.

Can't figure out HLS download

I've tried the following:
func setupAssetDownload() {
// Create new background session configuration.
let configuration = URLSessionConfiguration.background(withIdentifier: "123124123152")
// Create a new AVAssetDownloadURLSession with background configuration, delegate, and queue
let downloadSession = AVAssetDownloadURLSession(configuration: configuration,
assetDownloadDelegate: self,
delegateQueue: OperationQueue.main)
let url = URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")// HLS Asset URL
let asset = AVURLAsset(url: url!)
// Create new AVAssetDownloadTask for the desired asset
let downloadTask = downloadSession.makeAssetDownloadTask(asset: asset,
assetTitle: "assetTitle",
assetArtworkData: nil,
options: nil)
// Start task and begin download
print(downloadTask.debugDescription)
downloadTask?.resume()
}
and implemented
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
print("didFinishDownloadingTo \(location.relativePath)")
playOfflineAsset()
}
but the delegate method didFinishDownloadingTo is never called.
Also added didCompleteWithError delegate with no success.
My class is conforming to AVAssetDownloadDelegate.
AVAssetDownloadURLSession always work with real device only..So in your case its seems like you are tried on simulator.
Please go with Real device

URLSession downloadTask behavior when running in the background?

I have an app that needs to download a file which may be rather large (perhaps as large as 20 MB). I've been reading up on URLSession downloadTasks and how they work when the app goes to the background or is terminated by iOS. I'd like for the download to continue and from what I've read, that's possible. I found a blog post here that discusses this topic in some detail.
Based on what I've read, I first created a download manager class that looks like this:
class DownloadManager : NSObject, URLSessionDownloadDelegate, URLSessionTaskDelegate {
static var shared = DownloadManager()
var backgroundSessionCompletionHandler: (() -> Void)?
var session : URLSession {
get {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
}
private override init() {
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
if let completionHandler = self.backgroundSessionCompletionHandler {
self.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
if let sessionId = session.configuration.identifier {
log.info("Download task finished for session ID: \(sessionId), task ID: \(downloadTask.taskIdentifier); file was downloaded to \(location)")
do {
// just for testing purposes
try FileManager.default.removeItem(at: location)
print("Deleted downloaded file from \(location)")
} catch {
print(error)
}
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite > 0 {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
let progressPercentage = progress * 100
print("Download with task identifier: \(downloadTask.taskIdentifier) is \(progressPercentage)% complete...")
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Task failed with error: \(error)")
} else {
print("Task completed successfully.")
}
}
}
I also add this method in my AppDelegate:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
DownloadManager.shared.backgroundSessionCompletionHandler = completionHandler
// if the app gets terminated, I need to reconstruct the URLSessionConfiguration and the URLSession in order to "re-connect" to the previous URLSession instance and process the completed download tasks
// for now, I'm just putting the app in the background (not terminating it) so I've commented out the lines below
//let config = URLSessionConfiguration.background(withIdentifier: identifier)
//let session = URLSession(configuration: config, delegate: DownloadManager.shared, delegateQueue: OperationQueue.main)
// since my app hasn't been terminated, my existing URLSession should still be around and doesn't need to be re-created
let session = DownloadManager.shared.session
session.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) -> Void in
// downloadTasks = [URLSessionDownloadTask]
print("There are \(downloadTasks.count) download tasks associated with this session.")
for downloadTask in downloadTasks {
print("downloadTask.taskIdentifier = \(downloadTask.taskIdentifier)")
}
}
}
Finally, I start my test download like this:
let session = DownloadManager.shared.session
// this is a 100MB PDF file that I'm using for testing
let testUrl = URL(string: "https://scholar.princeton.edu/sites/default/files/oversize_pdf_test_0.pdf")!
let task = session.downloadTask(with: testUrl)
// I think I'll ultimately need to persist the session ID, task ID and a file path for use in the delegate methods once the download has completed
task.resume()
When I run this code and start my download, I see the delegate methods being called but I also see a message that says:
A background URLSession with identifier com.example.testapp.background already exists!
I think this is happening because of the following call in application:handleEventsForBackgroundURLSession:completionHandler:
let session = DownloadManager.shared.session
The getter for the session property in my DownloadManager class (which I took directly from the blog post cited previously) is always trying to create a new URLSession using the background configuration. As I understand it, if my app had been terminated, then this would be the appropriate behavior to "reconnect" to the original URLSession. But since may app is not being terminated but rather just going to the background, when the call to application:handleEventsForBackgroundURLSession:completionHandler: happens, I should be referencing the existing instance of URLSession. At least I think that's what the problem is. Can anyone clarify this behavior for me? Thanks!
Your problem is that you are creating a new session every time you reference the session variable:
var session : URLSession {
get {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
}
Instead, keep the session as an instance variable, and just get it:
class DownloadManager:NSObject {
static var shared = DownloadManager()
var delegate = DownloadManagerSessionDelegate()
var session:URLSession
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
override init() {
session = URLSession(configuration: config, delegate: delegate, delegateQueue: OperationQueue())
super.init()
}
}
class DownloadManagerSessionDelegate: NSObject, URLSessionDelegate {
// implement here
}
When I do this in a playground, it shows that repeated calls give the same session, and no error:
The session doesn't live in-process, it's part of the OS. You're incrementing reference count every time you access your session variable as written, which causes the error.