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
}
Related
I have a requirement to download large number of files - previously only one file could be downloaded at a time. The current design is such that when the user downloads a single file, a URLSession task is created and the progress/completion/fail is recorded using the delegate methods for urlsession. My question is, how can I leave a dispatch group in this delegate method? I need to download 10 files at a time, start the next 10 when the previous ten finishes. Right now, if I leave the dispatch group in the delegate method, the dispatch group wait waits forever. Here's what I've implemented so far:
self.downloadAllDispatchQueue.async(execute: {
self.downloadAllDispatchGroup = DispatchGroup()
let maximumConcurrentDownloads: Int = 10
var concurrentDownloads = 0
for i in 0..<files.count
{
if self.cancelDownloadAll {
return
}
if concurrentDownloads >= maximumConcurrentDownloads{
self.downloadAllDispatchGroup.wait()
concurrentDownloads = 0
}
if let workVariantPart = libraryWorkVariantParts[i].workVariantPart {
concurrentDownloads += 1
self.downloadAllDispatchGroup.enter()
//call method for download
}
}
self.downloadAllDispatchGroup!.notify(queue: self.downloadAllDispatchQueue, execute: {
DispatchQueue.main.async {
}
})
})
In the delegates:
func downloadDidFinish(_ notification: Notification){
if let dispatchGroup = self.downloadAllDispatchGroup {
self.downloadAllDispatchQueue.async(execute: {
dispatchGroup.leave()
})
}
}
Is this even possible? If not, how can I achieve this?
If downloadAllDispatchQueue is a serial queue, the code in your question will deadlock. When you call wait, it blocks that current thread until it receives the leave call(s) from another thread. If you try to dispatch the leave to a serial queue that is already blocked with a wait call, it will deadlock.
The solution is to not dispatch the leave to the queue at all. There is no need for that. Just call it directly from the current thread:
func downloadDidFinish(_ notification: Notification) {
downloadAllDispatchGroup?.leave()
}
When downloading a large number of files, we often use a background session. See Downloading Files in the Background. We do this so downloads continue even after the user leaves the app.
When you start using background session, there is no need to introduce this “batches of ten” logic. The background session manages all of these requests for you. Layering on a “batches of ten” logic only introduces unnecessary complexities and inefficiencies.
Instead, we just instantiate a single background session and submit all of the requests, and let the background session manage the requests from there. It is simple, efficient, and offers the ability to continue downloads even after the user leaves the app. If you are downloading so many files that you feel like you need to manage them like this, it is just as likely that the end user will get tired of this process and may want to leave the app to do other things while the requests finish.
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)
I have cells that have buttons that trigger the downloading of their respective PDF from online. I want it so that only one download can occur at a time, and the other ones (if their button is clicked) wait for it to finish.
I cannot use any sort of queue, because the queue operation calls the download methods but does not wait for them to complete before moving on.
Is there any way that I can only move on once the did finish download function says that it is ready by passing a boolean or something? I am pretty lost here so any direction is greatly appreciated.
I cannot use any sort of queue, because the queue operation calls the download methods but does not wait for them to complete before moving on.
This can be accomplished using NSOperation Queues. The key is that your download tasks have to be async NSOperation subclasses where you mark the operation as finished when the download finishes. More importantly, these operations should be queued on a serial queue. Then, operations will be executed only one at a time in FIFO order.
However, it takes a bit of boilerplate to get NSOperations setup this way. Another good way to do it is using Dispatch Groups.
// A serial queue ensures only one operation is executed at a time, FIFO
let downloadsQueue = dispatch_queue_create("com.youapp.pdfdownloadsqueue", DISPATCH_QUEUE_SERIAL)
let downloadGroup = dispatch_group_create()
func queueDownload(from url: NSURL) {
// Register this download task with the group
dispatch_group_enter(downloadGroup)
// Async dispatch the download task to our serial queue,
// so that it returns control back without blocking the main thread
dispatch_async(downloadsQueue) {
downloadPDF(with: url) { (pdf, error) in
// handle PDF data / error
// { .. }
// leave the dispatch group in the completion method,
// notifying the group that this task is finished
dispatch_group_leave(downloadGroup)
}
}
}
func downloadPDF(with url: NSURL, completion: (pdf: NSData?, error: ErrorType?) -> ()) {
// make network request
// call completion with PDF data or error when the download request returns
}
I have a situation where i am using background fetch to call my data sync process, As the sync function is a heavy task, it is executed in a background thread.
here is my code,
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
print("Background Fetch")
Utilities.syncCompleted = false // declared as :> static var syncCompleted:Bool = false
BackgroundSync().startSync() // heavy background task, and iam updating [Utilities.syncCompleted = true) on thread completion
while Utilities.syncCompleted == false {
NSThread.sleepForTimeInterval(1) // sleep for sometime
}
if Utilities.syncCompleted{
completionHandler(UIBackgroundFetchResult.NewData)
}else {
completionHandler(UIBackgroundFetchResult.NoData)
}
}
Now i have some questions :
As background fetch is of 30 sec, if my task is not completed in 30 sec then what happens, because i wont be able to set completionHandler to .NoData or .Failure
Is there is any default completionHandler value which is set (like .NoData) if developer does not specify in 30 sec.
Is there any other better way to do this.
Thanks in advance
Actually, if you are starting a backgroundTask, you are not limited to 30s, but to 10 minutes or whatever is the current limit for those.
Background fetch is a full-on starting of your app, including access to the main thread, UI changes etc. Essentially equivalent to starting the app minus the actual on-screen display. Background task are much more limited in what you can do but are allowed to take longer.
Thus in your case I would not care much about returning proper value of NoData or NewData. Start your sync as background task and call completionHandler(UIBackgroundFetchResult.NewData).
If you want to get as fair to the system as possible, you can check application.backgroundTimeRemaining and then schedule dispatch_after just before that expiring. So if you task finished before it, it will send NewData/NoData as received, but if it takes longer, then your dispatch_after block will send NewData and be done with it.
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.