I'm using Alamofire to make a call to a webservice which takes a pretty long time to load. If the app goes into the background I get stuck with my loader when I return to the app. I imagine it's because the call never returns anything to my completion handler. How can I address this problem?
You can use background fetching to solve this problem. It can be done in the following way in Swift 3:
var backgroundTask: UIBackgroundTaskIdentifier? // global variable
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "backgroundTask") {
// Cleanup code should be written here so that you won't see the loader
UIApplication.shared.endBackgroundTask(self.backgroundTask!)
self.backgroundTask = UIBackgroundTaskInvalid
}
Call your alamofire service after this line. In the completion handler, end the task using the below lines.
UIApplication.shared.endBackgroundTask(self.backgroundTask!)
self.backgroundTask = UIBackgroundTaskInvalid
Please note that the app has some background time (backgroundTimeRemaining property) remaining before it enters the inactive state. You have to get your task done before that time. The handler is called shortly before the remaining background time reaches zero. Also, each call to the method beginBackgroundTask(withName:){} must be balanced by a matching call to the endBackgroundTask: method.
To make the code given above work, you need to adjust settings in your app. Go to "Targets" and click on "Capabilities" to make the following changes
After that, go to your info.plist file, and open it as Source to add the following code.
I hope this helps you. If you need more detailed information, these links might help
https://developer.apple.com/reference/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio
https://developer.apple.com/reference/uikit/uiapplication/1622970-endbackgroundtask
The problem can be solved by adding:
application.beginBackgroundTask { }
into func applicationDidEnterBackground(_ application: UIApplication)
and
application.endBackgroundTask(.invalid)
into func applicationWillEnterForeground(_ application: UIApplication)
Related
I'm trying to complete a task in the background using UIApplication.shared.beginBackgroundTask but for some reason the expirationHandler is never called. The task I'm trying to complete is a video export from photo library but sometimes the export cannot be completed in time while the user is using the app in the foreground.
This is the code I'm using :
func applicationDidEnterBackground(_ application: UIApplication) {
if backgroundTask == .invalid && UploadQueue.instance.hasMoreWork() {
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "ExportQueue") {
NSLog("DriveLog - System is requesting end. Still more work to do ...")
self.endBackgroundTask()
}
print("Invalid? \(backgroundTask == .invalid)")
NSLog("DriveLog - Starting background task: %i", backgroundTask.rawValue)
}
}
func endBackgroundTask() {
NSLog("DriveLog - End called")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
I'm also calling :
(UIApplication.shared.delegate as! AppDelegate).endBackgroundTask()
during my task if I finish it earlier.
However I never see my expirationHandler being called in the log.
I have also tried putting beginBackgroundTask when starting the task in foreground but I get a warning message about task expiration while being in foreground.
You have not understood what the expiration handler is. It is called only if your time expires. Hence the name.
As soon as you call begin, start your task in the next line (not in the expiration handler). And when you are finished, call end.
You thus need to end the background task in two places: in the expiration handler, and outside it after actually performing your task.
It is very important to call end in both places, because if you fail to do so, the system will decide that you are a bad citizen and will never grant you any extra background time at all.
So, this is the diagram of the flow you need to construct:
Also note that this has nothing to do with UIBackgroundModes. That's a totally different mechanism.
matt's answer covers everything. I'm just going to try to give the same answer in different words because your edit suggests that matt's answer wasn't clear to you. (Read it again, though, it really does cover everything I'm going to say here, just in different words.)
You should not call beginBackgroundTask in applicationDidEnterBackground. You call it when you start whatever task you want time for. In your example that's going to be somewhere inside of UploadQueue. You don't call beginBackgroundTask when going into the background. You call it when you're starting a task that you would like to finish even if you go into the background.
Background tasks generally do not belong to the UIAppDelegate. They belong to the thing that creates the task (in your case: UploadQueue). You can create all the background tasks you want. They cost almost nothing. It's not just one "I want background" at the app level. Read matt's flow chart closely.
It's unclear from your question why you expect the expiration handler to be called. Do you expect your task to task to take so long that the OS forces you to stop it? That's what the expiration handler is for. If you've built your system correctly, it should rarely be called. Your task should end long before it's expired.
For full docs on how to do this, see Extending Your App's Background Execution Time. In particular note the caution:
Don’t wait until your app moves to the background to call the beginBackgroundTask(withName:expirationHandler:) method. Call the method before performing any long-running task.
Trying to use BackgroundTasks for iOS 13+. Long running operations don't seem to work:
// in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "foo.bar.name", using: nil) { task in
print("start!")
task.expirationHandler = {
// Not executed
print("expired!")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// Not executed
print("finish!")
task.setTaskCompleted(success: true)
}
}
return true
}
func applicationDidEnterBackground(_ application: UIApplication) {
BGTaskScheduler.shared.cancelAllTaskRequests()
let request = BGProcessingTaskRequest(identifier: "foo.bar.name")
request.earliestBeginDate = nil // or Date(timeIntervalSinceNow: 0) or Date(timeIntervalSinceNow: 5)...
do {
try BGTaskScheduler.shared.submit(request)
} catch let e {
print("Couldn't submit task: \(e)")
}
}
I also tried using a queue with Operation (for which I modeled my flow synchronously). This also didn't work. As soon as there's something that takes a while to complete, it gets stuck.
It doesn't log anything else to the console, no errors, no expired task message. It shows the last message before the long running operation and that's it. I confirmed that it doesn't move forward by storing a preference and examining it when restarting. It's not stored.
I added "foo.bar.name" to the info.plist (in "Permitted background task scheduler identifiers") and enabled capabilities both for background fetch and background processing. I'm testing on an iPhone with iOS 13.3.1 and using Xcode 11.4.1.
Additional notes:
I've been starting the tasks immediately as described here: https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development
I also tested with Apple's demo project. It shows the same problem: The database cleaning operation doesn't complete (I added a log at the beginning of cleanDatabaseOperation.completionBlock and it never shows).
A couple of observations:
You should check the result code of register. And you should make sure you didn’t see your “Couldn't submit task” log statement.
Per that discussion in that link you shared, did you set your breakpoint immediately after the submit call? This accomplishes two things:
First, it makes sure you hit that line (as opposed, for example, to the SceneDelegate methods).
Second, if you just pause the app manually, some random amount of time after the app has gone into background, that’s too late. It has to be in that breakpoint immediately after you call submit. Then do e command. Then resume execution.
Anyway, when I do that, running your code, the BGProcessingTaskRequest ran fine. I’m running iOS 13.4.1 (and like you, Xcode 11.4.1).
I need to make an API call when the user terminates the app (force close). The straight forward implementation I did is as below.
In the app delegate, I added the following code.
func applicationWillTerminate(_ application: UIApplication) {
print("________TERMINATED___________")
testAPICall()
}
func testAPICall(){
let url = getURL()
let contentHeader = ["Content-Type": "application/json"]
Alamofire.request(url,
method: .put,
parameters: ["username": "abc#xyz.com"],
encoding: JSONEncoding.default,
headers: contentHeader).responseJSON { (response) -> Void in
print("-----")
}
}
However, the call is not being made. And on going through the documentation, I have found that I get only 5 seconds for completing the task in this method and above all, making api call is not a task to be done here. So I wonder, what would be the way to do this.
This is a two fold question
Phase 1: Ensuring API Call starts every time user terminates the app/ before it turns in active
You can always make use of expiration handler background mode of iOS application In your appdelegate
declare
var bgTask: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier(rawValue: 0);
and in your appdelegate
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
bgTask = application.beginBackgroundTask(withName:"MyBackgroundTask", expirationHandler: {() -> Void in
// Do something to stop our background task or the app will be killed
application.endBackgroundTask(self.bgTask)
self.bgTask = UIBackgroundTaskIdentifier.invalid
})
DispatchQueue.global(qos: .background).async {
//make your API call here
}
// Perform your background task here
print("The task has started")
}
Background expiration handler will ensure you will get enough time to start your API call every time you put your application turns inactive or gets terminated
Phase 2: Ensuring API call started finishes successfully
Though expiration handler might ensure that you get enough time to start your API call it can't ensure the successful completion of API call. What if API call takes longer and while the request is in flight and time runs out??
The only way you to ensure that API call gets successful once started is to make sure to use proper configuration for URLSession
As per docs
Background sessions let you perform uploads and downloads of content
in the background while your app isn't running.
link: https://developer.apple.com/documentation/foundation/nsurlsession?language=objc
So make use of Background session and use upload task. Rather than having a plain get/post API which you will hit with some parameter, ask your backend developer to accept a file and put all your param data in that file (if you have any) and start an upload task with background session.
Once the upload task starts with background session iOS will take care of its completion (unless u end up in a authentication challenge obviously) even after your app is killed.
This I believe is the closest you can get to ensure starting a API call and ensuring it finishes once app gets inactive/terminated. I kind a had a discussion with a apple developer regarding the same, and they agreed that this can be a probable solution :)
hope it helps
The main idea here is to make a sync call before app terminate
func applicationWillTerminate(_ application: UIApplication) {
let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0)
let request = NSMutableURLRequest(URL:url)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request,
completionHandler: {
taskData, _, error -> () in
dispatch_semaphore_signal(semaphore);
})
task.resume()
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}
Tips:
A dispatch semaphore is an efficient implementation of a traditional counting semaphore. Dispatch semaphores call down to the kernel only when the calling thread needs to be blocked. If the calling semaphore does not need to block, no kernel call is made.
You increment a semaphore count by calling the signal() method, and decrement a semaphore count by calling wait() or one of its variants that specifies a timeout.
here is simple way to achieve this task-
func applicationWillTerminate(_ application: UIApplication) {
let sem = DispatchSemaphore(value: 0)
startSomethingAsync(completionHandler: {
sem.signal()//When task complete then signal will call
})
sem.wait()//waiting until task complete
}
My code in appdelegate for background fetch is never fully run. I have the background fetch option turned on and the plist updated.
I trigger the code by pressing Debug > Simulate Background Fetch
This is the code
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
User.getNotifications(User.getUserDetails()["id"].string!, callback: {(notifications) in
//update notification badge count
notificationBadgeCount = X
})
}
'User.getNotifications' looks like this
getNotifications(id: String, callback...){
alamofire.request(.GET....){ jsonResponse in
//GETS HERE
callback(jsonResponse)
}
}
When triggering the simulated background fetch, the alamofire GET request is sent and data is returned (I've checked the server and the call is sent), however, the app seems to suspend at (//GETS HERE) in the getNotifications call, so the rest the code in the background fetch (//update notification badge count) is never run.
The code seems to time out. I'm supposed to get 30s however it seems to time out in 5s or something.
Any idea why that section of code isn't executed?
NOTE: If I re-open the app manually, then the rest of the code executes.
performFetch has an incoming function called completionHandler. You must call that function to complete the fetch and stop the countdown clock. You are not doing that and you thus are timing out and the app is suspended.
I have an app that the first thing it does is to register itself in an API, with a simple HTTP POST. I've been doing this in func application(application: UIApplication, willFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?), as Apple states that this is the point do to URL calls. As it is important to never block the main thread, this call is done async.
The problem is that as it is done async, the first screen opens and immediately a call to the API is done. As this is faster then the first API call, the second call gets a 401.
In order to avoid that, I am doing a very cheesy thing before starting the second call:
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// do some task
while (InformationFromFirsCall == nil)
{
sleep(1)
}
}
Is there a better strategy to do this? I was thinking about using a dispatch_once at the beginning of every call to the API and implementing the code inside the callback of InformationFromFirstCall.
Is that reasonable?
Thanks!
Instead of using sleep, you can save CPU time by using a semaphore:
// declared somewhere where you can access it
dispatch_semaphore_t sema;
// willFinishLaunching
sema = dispatch_semaphore_create(0);
// when your async call completes
dispatch_semaphore_signal(sema);
// in your main UI — this is the same as the sleep call you have, but doesn't waste CPU time
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
A semaphore is an efficient mechanism for forcing one part of your app to wait while another app is doing something — a call to dispatch_semaphore_wait causes the current thread to hang until the semaphore's value becomes non-zero, which happens when dispatch_semaphore_signal is called.
See this answer for more details.
Note: this is C code; you will need to adapt it to Swift syntax if you are coding your app in Swift.