Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use session.uploadTask in background i am getting crash

Tags:

ios

swift

I am getting crash

it says *** Terminating app due to uncaught exception 'NSGenericException', reason: 'Completion handler blocks are not supported in background sessions. Use a delegate instead.'

var Boundary = "\(boundary.generateBoundaryString())_boundary"
private lazy var session: URLSession = {
    let config = URLSessionConfiguration.background(withIdentifier: "MyUniqeId")
    config.isDiscretionary = true
    config.sessionSendsLaunchEvents = true
    return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()

func webServiceForUploadImages(urlStr:String,params:[String:String],fileUrl:String,imageData:Data,success :@escaping (AppMedia) -> Void ,failure:@escaping (NSError) -> Void) -> Void{
    let url = Constant.BASE_URL + urlStr
    print(url)
    if(reachAbility.connection != .none){
        var request = URLRequest(url: URL(string: url)!)
        request.httpMethod = "POST"
        request.allHTTPHeaderFields = Header.headers()
        request.setValue("multipart/form-data; boundary=\(Boundary)", forHTTPHeaderField: "Content-Type")
        let data = try! createBody(with: params, filePathKey: "file", paths: [fileUrl], boundary: "\(Boundary)", imageData: imageData)

        session.uploadTask(with: request, from: data) { (data, response, err) in
            if response != nil{
                guard let response = response as? HTTPURLResponse else {return}
                handleError.shared.HandleReponseTokenExpireError(dataResponse: response, success: { (response) in
                })
                if(err != nil){
                    print("\(err!.localizedDescription)")
                }
                guard let responseData = data else {
                    print("no response data")
                    return
                }
                if let responseString = String(data: responseData, encoding: .utf8) {
                    DispatchQueue.main.async {
                        let dict = Utility.jsonToDict(str: responseString)
                        let mediaDict = AppMedia(fromDictionary: dict as! [String : Any])
                        Constant.KAppDelegate.hideProgressHUD()
                        success(mediaDict)
                    }

                   // print("uploaded to: \(responseString)")
                }
            }else{
                DispatchQueue.main.async {
                    failure(err! as NSError)
                    Constant.KAppDelegate.hideProgressHUD()
                    Constant.KAppDelegate.showErrorMessage(title: "Error", message: Constant.ServerError, msgType: .error)
                }
            }

            }.resume()
    }else{
         self.showErrorMsg(str: Constant.ConnectivityError)
    }
}

let config = URLSessionConfiguration.background(withIdentifier: "MyUniqeId") using this gives me crash

like image 972
Maddy Avatar asked Jan 26 '23 19:01

Maddy


1 Answers

To upload using background URLSessionConfiguration there are a few special considerations:

  1. Cannot use completion handlers (because the app might not be running when you finish the upload). You must use delegate-base methods, e.g. uploadTask(with:fromFile:).

    For example:

     func startUpload(for request: URLRequest, from data: Data) throws -> URLSessionUploadTask {
         let fileURL = URL(fileURLWithPath: NSTemporaryDirectory())
             .appendingPathComponent(UUID().uuidString)
         try data.write(to: fileURL)
         let task = session.uploadTask(with: request, fromFile: fileURL)
         task.resume()
    
         return task
     }
    

    That obviously assumes that you’ve specified your delegate and implemented the appropriate delegate methods:

     extension BackgroundSession: URLSessionDelegate {
         func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
             DispatchQueue.main.async {
                 self.savedCompletionHandler?()
                 self.savedCompletionHandler = nil
             }
         }
     }
    
     extension BackgroundSession: URLSessionTaskDelegate {
         func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
             if let error = error {
                 print(error)
                 return
             }
             print("success")
         }
     }
    
  2. Note, we cannot use uploadTask(with:from:) because that’s using Data for that second parameter, which is not allowed for background sessions. Instead one must save the body of the request into a file and then use uploadTask(with:fromFile:).

  3. Remember to handle the scenario where the upload finishes when your app is not running. Namely, the app delegate’s handleEventsForBackgroundURLSession must capture the completion handler. For example, I’ll have a property in my BackgroundSession to save the completion handler:

     func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { 
         BackgroundSession.shared.savedCompletionHandler = completionHandler
     }
    

    And then you want to implement urlSessionDidFinishEvents(forBackgroundURLSession:) and call the saved completion handler:

     extension BackgroundSession: URLSessionDelegate {
         func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
             DispatchQueue.main.async {
                 self.savedCompletionHandler?()
                 self.savedCompletionHandler = nil
             }
         }
     }
    

By the way, Downloading Files in the Background discusses many of these considerations (e.g. delegate API rather than closure based API, app delegate issues, etc.). It even discusses the requirements that upload tasks are file-based, too.


Anyway, here is a sample BackgroundSession manager:

import os.log

// Note, below I will use `os_log` to log messages because when testing background URLSession
// you do not want to be attached to a debugger (because doing so changes the lifecycle of an
// app). So, I will use `os_log` rather than `print` debugging statements because I can then
// see these logging statements in my macOS `Console` without using Xcode at all. I'll log these
// messages using this `OSLog` so that I can easily filter the macOS `Console` for just these
// logging statements.

private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: #file)

class BackgroundSession: NSObject {
    var savedCompletionHandler: (() -> Void)?
    
    static var shared = BackgroundSession()
    
    private var session: URLSession!
    
    private override init() {
        super.init()

        let identifier = Bundle.main.bundleIdentifier! + ".backgroundSession"
        let configuration = URLSessionConfiguration.background(withIdentifier: identifier)

        session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
    }
}

extension BackgroundSession {
    @discardableResult
    func startUpload(for request: URLRequest, from data: Data) throws -> URLSessionUploadTask {
        os_log("%{public}@: start", log: log, type: .debug, #function)

        let fileURL = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent(UUID().uuidString)
        try data.write(to: fileURL)
        let task = session.uploadTask(with: request, fromFile: fileURL)
        task.resume()

        return task
    }
}

extension BackgroundSession: URLSessionDelegate {
    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        os_log(#function, log: log, type: .debug)

        DispatchQueue.main.async {
            self.savedCompletionHandler?()
            self.savedCompletionHandler = nil
        }
    }
}

extension BackgroundSession: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            os_log("%{public}@: %{public}@", log: log, type: .error, #function, error.localizedDescription)
            return
        }
        os_log("%{public}@: SUCCESS", log: log, type: .debug, #function)
    }
}

extension BackgroundSession: URLSessionDataDelegate {
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didBecome downloadTask: URLSessionDownloadTask) {
        os_log(#function, log: log, type: .debug)
    }
    
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        os_log("%{public}@: received %d", log: log, type: .debug, #function, data.count)
    }
}

And, of course, my app delegate:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    ...

    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        BackgroundSession.shared.savedCompletionHandler = completionHandler
    }
}

Needless to say, that’s a lot to worry about with uploads in conjunction with background URLSession. If you’re uploading huge assets (e.g. videos) over slow connections, perhaps you need that. But the other (simpler) alternative is to use a default URLSession configuration, and just tell the OS that even if the user leaves your app, request a little more time to finish the upload. Just use standard completion handler pattern with default URLSession, and marry that with the techniques outlined in Extending Your App's Background Execution Time. Now, that only buys you 30 seconds or so (it used to be 3 minutes in older iOS versions), but often that’s all we need. But if you think it may take more than 30 seconds to finish the uploads, then you’ll need background URLSessionConfiguration.

like image 123
Rob Avatar answered Jan 29 '23 10:01

Rob