iOS: Perform upload task while app is in background - ios

Is there really no way to run an UPLOAD task while an iOS app is in the background? This is ridiculous. Been looking at various stuff like NSURLSessionUploadTask, dispatch_after and even NSTimer, but nothing works for more than the meager 10 seconds the app lives after being put in the background.
How do other apps that have uploads work? Say, uploading an image to Facebook and putting the app in the background, will that cancel the upload?
Why cannot iOS have background services or agents like Android and Windows Phone has?
This is a critical feature of my app, and on the other platforms is works perfectly.
Any help is appreciated :(

You can continue uploads in the background with a “background session”. The basic process of creating a background URLSessionConfiguration with background(withIdentifier:) is outlined in Downloading Files in the Background. That document focuses on downloads, but the same basic process works for upload tasks, too.
Note:
you have to use the delegate-based URLSession;
you cannot use the completion handler renditions of the task factory methods with background sessions;
you also have to use uploadTask(with:fromFile:) method, not the Data rendition ... if you attempt to use uploadTask(with:from:), which uses Data for the payload, with background URLSession you will receive exception with a message that says, “Upload tasks from NSData are not supported in background sessions”; and
your app delegate must implement application(_:handleEventsForBackgroundURLSession:completionHandler:) and capture that completion handler which you can then call in your URLSessionDelegate method urlSessionDidFinishEvents(forBackgroundURLSession:) (or whenever you are done processing the response).
By the way, if you don't want to use background NSURLSession, but you want to continue running a finite-length task for more than a few seconds after the app leaves background, you can request more time with UIApplication method beginBackgroundTask. That will give you a little time (formerly 3 minutes, only 30 seconds in iOS 13 and later) complete any tasks you are working on even if the user leave the app.
See Extending Your App's Background Execution Time. Their code snippet is a bit out of date, but a contemporary rendition might look like:
func initiateBackgroundRequest(with data: Data) {
var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
// Request the task assertion and save the ID.
backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish Network Tasks") {
// End the task if time expires.
if backgroundTaskID != .invalid {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
backgroundTaskID = .invalid
}
}
// Send the data asynchronously.
performNetworkRequest(with: data) { result in
// End the task assertion.
if backgroundTaskID != .invalid {
UIApplication.shared.endBackgroundTask(backgroundTaskID)
backgroundTaskID = .invalid
}
}
}
Please don’t get lost in the details here. Focus on the basic pattern:
begin the background task;
supply a timeout clause that cleans up the background task if you happen to run out of time;
initiate whatever you need to continue even if the user leaves the app; and
in the completion handler of the network request, end the background task.

class ViewController: UIViewController, URLSessionTaskDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://0.0.0.0")!
let data = "Secret Message".data(using: .utf8)!
let tempDir = FileManager.default.temporaryDirectory
let localURL = tempDir.appendingPathComponent("throwaway")
try? data.write(to: localURL)
let request = URLRequest(url: url)
let config = URLSessionConfiguration.background(withIdentifier: "uniqueId")
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.uploadTask(with: request, fromFile: localURL)
task.resume()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
print("We're done here")
}

Related

iOS long task in background

After reading several similar questions I don't find anything that fits into my problem.
I have an app that analyzes CSV files (some kind of big data analyzer app). So far CSV files were small and the analysis didn't take too long.
But now we need to support very large CSV files and the analysis takes 5-10 minutes.
If the user closes the app, this process is paused and when the users opens the app again the process is resumed. We need that the analysis continues while the app is in the background.
After reading about different possibilities of running in background I have this:
Use BGTaskScheduler
I'm not sure how to do it. I've followed this official tutorial https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app?language=objc and I've implemented this in my AppDelegate method:
didFinishLaunchingWithOptions {
...
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.mydomain.myapp.myproccess", using: nil) { task in
self.scheduleAppRefresh()
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
...
}
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.mydomain.myapp.myproccess")
request.earliestBeginDate = Date(timeIntervalSinceNow: 1)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}
func handleAppRefresh(task: BGAppRefreshTask) {
scheduleAppRefresh()
// Run my task, but how??
}
I don't know how to call the code inside one of my views (in fact one of my presenter cause I'm using VIPER) from appDelegate.
I'm not sure if this is how it works. If I'm not wrong, this BGTaskScheduler is to schedule a task when app goes to background.
Use beginBackgroundTask.
My code was this:
func csvAnalysis() {
DispatchQueue.global(qos: .userInitiated).async {
// Task to analyze CSV
}
}
and now is this:
func csvAnalysis() {
DispatchQueue.global(qos: .userInitiated).async {
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "mytask") {
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
}
// Task to analyze CSV
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
}
}
But this way, the process keeps pausing after entering background and resuming after opening the app again.
So, is it possible to execute this long task while the app is in background?

swift ios 10 execute code asynchronously or in the background

When user launch the app or finish editing the data I need to update local notifications, basically it takes around 2-3 seconds in async way. I need to make sure that this code executes even if app leave foreground. What I have now:
func buildLocalNotifications()
let dq = DispatchQueue.global(qos: .userInteractive)
dq.async {
//recreate the notifications
}
}
And I can call this method from didFinishLaunchingWithOptions or when user save the form and everything works like a charm while app stays active for more then 3-4 seconds and its not blocking UI of course.. but if user lock the screen or terminate the app - this code won;t finished and notifications won't be created. How to safely execute sensitive code?
What is coming on my mind - show up a loader while performing this action - but it will block the user interaction
Ok I found the solution for the task which requires some time and should not be interrupted when app leaves foreground.
So we need beginBackgroundTask and endBackgroundTask
Small manager which you can use to execute code even when app is not in foreground
class BackgroundTaskManager {
let backgroundDQ = DispatchQueue.global(qos: .background)
var backgroundUpdateTask: UIBackgroundTaskIdentifier!
init(withName: String) {
self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(withName: withName) {}
}
/* Using completion handler to know when code is done*/
func runBackgroundTask(withCode: #escaping (_ cH: #escaping () -> Void) -> Void)
{
backgroundDQ.async {
withCode() {
self.endBackgroungTask()
}
}
}
func endBackgroungTask() {
if backgroundUpdateTask != nil && backgroundUpdateTask != UIBackgroundTaskInvalid {
UIApplication.shared.endBackgroundTask(backgroundUpdateTask)
backgroundUpdateTask = UIBackgroundTaskInvalid
}
}
}
And you can use it like
let taskManager = BackgroundTaskManager(withName: "LocalNotifications")
taskManager.doBackgroundTask() { (cH) in
//Your code goes here
//Send back completion handler so system knows when to finish background task
cH()
}
More information you can find on the Medium
If you want to make sure your code gets executed even if the user closes your app, you need to call your function in applicationWillTerminate. However, you only have ~5 seconds to execute code, before the system closes your app, so asynchronous execution is not encouraged here. It also doesn't matter if you execute code synchronously, since the user already quit your app, so you won't be blocking any UI updates.
Try to excute your code in background
DispatchQueue.global(qos: .background).async {
// your code here
}

How to write code that will not pause when user press home button

I want to implement image sync in my ios app. for this what i have done is
take a photo
upload that photo to firebase
get the firebase stored url and send it to an api server so that server stores that info to a database
It works ok as long as the app is running, but when I exit out from the app pressing home button everything pauses.
So how can run a code that will not pause if app goes to home page?
As per this documentation,
For tasks that require more execution time to implement, you must
request specific permissions to run them in the background without
their being suspended. In iOS, only specific app types are allowed to
run in the background:
Apps that play audible content to the user while in the background,
such as a music player app
Apps that record audio content while in the
background Apps that keep users informed of their location at all
times, such as a navigation app
Apps that support Voice over Internet
Protocol (VoIP) Apps that need to download and process new content
regularly
Apps that receive regular updates from external accessories
Apps that implement these services must declare the services they
support and use system frameworks to implement the relevant aspects of
those services
Declaring the services lets the system know which
services you use, but in some cases it is the system frameworks that
actually prevent your application from being suspended.
Link: https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html
Here is how to implement (taken from this answer by Ashley Mills):
func doUpdate () {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let taskID = beginBackgroundUpdateTask()
var response: NSURLResponse?, error: NSError?, request: NSURLRequest?
let data = NSURLConnection.sendSynchronousRequest(request, returningResponse: &response, error: &error)
// Do something with the result
endBackgroundUpdateTask(taskID)
})
}
func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
return UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({})
}
func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.sharedApplication().endBackgroundTask(taskID)
}
[Swift3] This is kind of worked for me
func doUpdate () {
DispatchQueue.global(qos: .background).async {
let taskID = self.beginBackgroundUpdateTask()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: {
print("printing after 10 min")
})
DispatchQueue.main.async {
self.endBackgroundUpdateTask(taskID: taskID)
}
}
}
func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
return UIApplication.shared.beginBackgroundTask(expirationHandler: {})
}
func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.shared.endBackgroundTask(taskID)
}
Not sure what is the max time for this to complete since i see there is a expirationHandler

Send request in applicationWillTerminate

In my app I need to send some instructions to server when the user terminated an app. In applicationWillTerminate func I tried to send it, but it never came to server. I tried to use Alamofire and native URLSession but it doesn't work. Does anybody know how can I send it?
I use this code
let request = "\(requestPrefix)setDriverOrderStatus"
if let url = URL(string:request) {
var parameters : [String : String] = [:]
parameters["access_token"] = UserSession.accessToken
parameters["driver_id"] = UserSession.userID
parameters["status"] = status
var req = URLRequest(url: url)
req.httpMethod = HTTPMethod.put.rawValue
do {
req.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
} catch let error {
print(error.localizedDescription)
}
_ = URLSession.shared.dataTask(with: req, completionHandler: { data, response, error in
guard error == nil else {
print(error ?? "error")
return
}
guard let data = data else {
print("Data is empty")
return
}
let json = try! JSONSerialization.jsonObject(with: data, options: [])
print(json)
}).resume
}
One solution that worked for me is to add sleep at the end of the applicationWillTerminate function like this :
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
// Saves changes in the application's managed object context before the application terminates.
// HERE YOU will make you HTTP request asynchronously
self.postLogoutHistory()
// 3 is the number of seconds in which you estimate your request
// will be finished before system terminate the app process
sleep(3)
print("applicationWillTerminate")
// self.saveContext()
}
put breakpoint in applicationWillTerminate and check that, function is getting called or not because applicationWillTerminate is not called everytime when application is getting terminated, especially when user quit application manually from multitasking window, applicationWillTerminate will not get called! When system terminates the application at that time applicationWillTerminate will get called and you will got approximately five seconds to complete your task!! So, it is not good idea to perform network related task on applicationWillTerminate!!
Refer Apple Documentation for applicationWillTerminate, It states,
This method lets your app know that it is about to be terminated and
purged from memory entirely. You should use this method to perform any
final clean-up tasks for your app, such as freeing shared resources,
saving user data, and invalidating timers. Your implementation of this
method has approximately five seconds to perform any tasks and return.
If the method does not return before time expires, the system may kill
the process altogether.
For apps that do not support background execution or are linked
against iOS 3.x or earlier, this method is always called when the user
quits the app. For apps that support background execution, this method
is generally not called when the user quits the app because the app
simply moves to the background in that case. However, this method may
be called in situations where the app is running in the background
(not suspended) and the system needs to terminate it for some reason.
After calling this method, the app also posts a
UIApplicationWillTerminate notification to give interested objects a
chance to respond to the transition.

WatchOS 3 WKApplicationRefreshBackgroundTask didReceiveChallenge

I have finally (ignoring the sample code which I never saw work past "application task received, start URL session") managed to get my WatchOS3 code to start a background URL Session task as follows:
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for task in backgroundTasks {
if let refreshTask = task as? WKApplicationRefreshBackgroundTask {
// this task is completed below, our app will then suspend while the download session runs
print("application task received, start URL session")
let request = self.getRequestForRefresh()
let backgroundConfig = URLSessionConfiguration.background(withIdentifier: NSUUID().uuidString)
backgroundConfig.sessionSendsLaunchEvents = true
backgroundConfig.httpAdditionalHeaders = ["Accept":"application/json"]
let urlSession = URLSession(configuration: backgroundConfig, delegate: self, delegateQueue: nil)
let downloadTask = urlSession.downloadTask(with: request)
print("Dispatching data task at \(self.getTimestamp())")
downloadTask.resume()
self.scheduleNextBackgroundRefresh(refreshDate: self.getNextPreferredRefreshDate())
refreshTask.setTaskCompleted()
}
else if let urlTask = task as? WKURLSessionRefreshBackgroundTask {
//awakened because background url task has completed
let backgroundConfigObject = URLSessionConfiguration.background(withIdentifier: urlTask.sessionIdentifier)
self.backgroundUrlSession = URLSession(configuration: backgroundConfigObject, delegate: self, delegateQueue: nil) //set to nil in task:didCompleteWithError: delegate method
print("Rejoining session ", self.backgroundUrlSession as Any)
self.pendingBackgroundURLTask = urlTask //Saved for .setTaskComplete() in downloadTask:didFinishDownloadingTo location: (or if error non nil in task:didCompleteWithError:)
} else {
//else different task, not handling but must Complete all tasks (snapshot tasks hit this logic)
task.setTaskCompleted()
}
}
}
However, the issue I am now seeing is that my delegate method
urlSession:task:didReceiveChallenge: is never being hit, so I cannot get my download to complete. (I have also added the session level urlSession:didReceiveChallenge: delegate method and it is also not being hit).
Instead I immediately hit my task:didCompleteWithError: delegate method which has the error:
"The certificate for this server is invalid. You might be connecting to a server that is pretending to be ... which could put your confidential information at risk."
Has anyone gotten the background watch update to work with the additional requirement of hitting the didReceiveChallenge method during the background URL session?
Any help or advice you can offer is appreciated.
As it turns out the server certificate error was actually due to a rare scenario in our test environments. After the back end folks gave us a work around for that issue this code worked fine in both our production and test environments.
I never hit urlSession:task:didReceiveChallenge: but it turned out I did not need to.
Made a minor un-related change:
Without prints/breakpoints I was sometimes hitting task:didCompleteWithError Error: like a ms before I hit downloadTask:didFinishDownloadingTo location:.
So I instead set self.pendingBackgroundURLTask completed in downloadTask:didFinishDownloadingTo location:. I only set it completed in task:didCompleteWithError Error: if error != nil.
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
//Complete task only if error, if no error it will be completed when download completes (avoiding race condition)
if error != nil {
self.completePendingBackgroundTask()
}
}
func completePendingBackgroundTask()
{
//Release the session
self.backgroundUrlSession = nil
//Complete the task
self.pendingBackgroundURLTask?.setTaskCompleted()
self.pendingBackgroundURLTask = nil
}
Hope someone else finds this helpful.

Resources