My iOS app receives notification to refresh its state from our main service. Right now, we are using Alamofire to fetch the latest state and we play a continues sound when there is updated data. Our devices are locked in guided mode to stop them from turning off and provide kiosk experience.
We are making changes such that the device can go to sleep after xx minutes of inactivity. However, we ran into a problem where the device was not getting results back from Alamofire even though the request was sent successfully (based on our logs on the api side).
As I am using Alamofire 4, I have setup a singleton with backgroundsessionmanager which is how AF requests are sent now. But the challenge is that requests are sent intermittently and fail most of the time when the device is sleeping with this error:
Domain=NSURLErrorDomain Code=-997 "Lost connection to background transfer service"
Here is my code for singleton (and I have associated code in AppDelegate):
class Networking {
static let sharedInstance = Networking()
public var sessionManager: Alamofire.SessionManager // most of your web service clients will call through sessionManager
public var backgroundSessionManager: Alamofire.SessionManager // your web services you intend to keep running when the system backgrounds your app will use this
private init() {
self.sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
self.backgroundSessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.background(withIdentifier: "com.test.app"))
}
}
Here is my code for sending the request:
let NetworkManager = Networking.sharedInstance.backgroundSessionManager
DispatchQueue.main.async(execute: {
NetworkManager.request(self.api_url, method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).responseJSON{ response in
switch (response.result){
case .success:
if let jsonResp = response.result.value{
print("got the response")
print(jsonResp)
// parse results
}
case .failure:
print("Network Error: \(response.error)")
}
}
})
I am hoping to get some help to resolve this issue as I am unable to rootcause the inconsistent behavior. I have read in some places that Apple/iOS will only allow upload/download when the app is in background mode instead of requests.
Yes, background sessions only permit upload and download tasks, not data tasks. They also only permit delegate-based requests, not completion handler-based requests. This answer outlines many of the considerations when doing this in conjunction with Alamofire.
But this begs the question as to whether you really want to use a background session at all. When your app is awaken for background fetch, if you’re able to finish you request within a reasonable amount of time (e.g. 30 seconds), you should probably consider a standard session, not a background session. It’s a lot simpler.
Do not conflate an app running in the “background” with a “background” URLSessionConfiguration: They’re completely different patterns. Just because your app is running in the background, that doesn’t mean you have to use background URLSessionConfiguration. If your app is running (whether in foreground or in the background), then a standard session is fine. You only need background session if you want it to continue after the app is suspended (or is terminated) and you’re willing to encumber yourself with all the extra overhead that background sessions entail.
Background sessions are not intended for requests performed while the app is running in the background. They’re intended for requests that will continue after your app is suspended (and even if it eventually is terminated in the course of its natural lifecycle). That means that background sessions are ideal for slow requests that cannot be completed in a reasonable amount of time, e.g., downloading video asset, downloading many large image assets or documents, etc.
But if you’re just performing a routine GET/POST request that will complete in a reasonable amount of time, consider not using background URLSessionConfiguration, but just do normal request and call the background fetch completion handler when your request is done (i.e., in your network request’s completion handler).
Related
I'm uploading a lot of files to a server using URLSession, because the uploaded files can be big I'm using URLSessionConfiguration.background so that my uploads can continue in the background.
My url session is declared like this:
urlSessionConfiguration = URLSessionConfiguration.background(withIdentifier: UploadQueue.uploadQueueIdentifier)
urlSessionConfiguration.sessionSendsLaunchEvents = true
urlSessionConfiguration.shouldUseExtendedBackgroundIdleMode = true
urlSessionConfiguration.allowsCellularAccess = true
urlSessionConfiguration.sharedContainerIdentifier = appGroup
backgroundUploadSession = URLSession(configuration: urlSessionConfiguration, delegate: self, delegateQueue: nil)
URLSessionUploadTask are always created in the foreground (we use background session only to ensure that tasks can finish).
var request = try! URLRequest(url: url, method: .put)
backgroundUploadSession.uploadTask(with: request, fromFile: fileUrl).resume()
All the necessary delegates are implemented and called normally when uploading ~ 100 files and staying in the foreground.
However when uploading a lot of files (~1000), the first files upload correctly but after some time the session seems "stuck" and no callback are delivered. (Still with the app in the foreground)
I noticed that if I just wait the upload restarts after ~5 minutes.
I tried replacing URLSessionConfiguration.background(withIdentifier: UploadQueue.uploadQueueIdentifier) with URLSessionConfiguration.default and it's working perfectly in the foreground.
Is it a bug with with URLSessionConfiguration.background or am I doing something wrong ?
After investigating there were multiple problems:
We were expecting that URLSessionConfiguration.httpMaximumConnectionsPerHost would limit the number of open connections. However this limit doesn't seem to be respected when using http/2 or when the server is behind a proxy.
Solution: Do your own connection limiting (eg. with DispatchGroup)
Even when limiting the number of concurrent uploads, the background sessions seem stuck after some time even when the app is in the foreground. There is a rate limiter for background session but the documentation says it is not endorsed while the app is in the background.
Solution: This was discussed on Apple Developer Forum and this is a bug in the CF Network Framework. A bug report has been made. For the time being, the solution is to simply not create too much background session with small files.
I am trying to build a sequential download manager where the user can initiate up to 1000 download tasks at once but only 2 will actually download and the rest will be put on hold, until one of the two finishes so that another one of the 998 can start.
Because there is no way to add a downloadTask then have that task wait for others to complete then automatically start, I have built a download queue myself and will only create new downloadTask when the old one completes.
I have read the documentations and am well aware of the mechanism of iOS's handling of background download event, and what methods I need to implement in order to handle them correctly. However, I am unable to find anything on whether it is safe and reliable to start a new downloadTask when the old one completes IN BACKGROUND.
The Main Question:
When iOS relaunches my app in background to inform me that my download tasks are finished, can I reliably create a new downloadTask and add it to the current session? If so, when that task finishes as well, will the system relaunch my app AGAIN to tell me it's finished? If so, then I can create an infinite loop of adding new tasks when the old backgroundTask finish.
Code Example
Would the following code reliable to get the entire download queue downloaded(say, 1000 items) if I download ONLY one item at a time and meanwhile the app stays entirely in background?
extension MyClass: URLSessionDelegate {
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
// This is called when background tasks are done
// Let's add a new background tasks WHILE app has just been
// relaunched IN BACKGROUND
if let nextURL = myURLQueue.removeFirst() {
session.downloadTask(with: URLRequest(url: nextURL))
}
DispatchQueue.main.async {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let completionHandler = appDelegate.backgroundSessionCompletionHandler {
appDelegate.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
}
}
You are creating the URLSessionDownloadTask, but never starting it with resume().
FWIW, even if you fix the above, you should recognize that requests that are submitted while the app is running in background mode will always be submitted in discretionary mode. As the docs for isDiscretionary say:
For transfers started while your app is in the background, the system always starts transfers at its discretion—in other words, the system assumes this property is true and ignores any value you specified.
Also, recognize that having the system fire up your app every time a request is done in order to submit the next request is both inefficient (because your app will constantly fire up and go back into background) and slow (because concurrent requests reduce latency effects). It’s generally better to just submit all of the requests up front, while the app is active, and let the background session manage how many it will allow to run concurrently.
I'm working on an iOS app which makes use of the location background mode to track user visits and then sends some data over to my server. However, I have been experiencing some weird network communication problems. The only symptom is that not all gathered data gets sent to the server.
Here is more information on the problem:
My server makes logs of everything received. There were no server-side errors and every client request was successfully logged.
The client app creates a local notification when the locationManager:didVisit: method is called. This notification appears as expected when you arrive and depart at some location. Then, it calls the server over HTTPS and posts another notification, which doesn't appear every time. The whole setup looks like this:
// This code is executed from locationManager:didVisit: when the app is in background.
let myVisit: CLVisit! = ... // the received visit
self.postLocalNotification("Visit received!", visit: myVisit)
let task = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler(nil)
Alamofire.request(.POST, apiMethod("visit"), parameters: params, encoding: .JSON)
.responseJSON { (request, response, JSON, error) in
// This gets executed only some time, wtf?
self.postLocalNotification("Visit reported!", visit: myVisit)
UIApplication.sharedApplication().endBackgroundTask(task)
}
Therefore, I conclude I'm doing something wrong, yet I don't see what. I have checked the article on background app execution and my app seems to comply with it. What else could I be missing?
Is your app in registered to support background mode?
Since you receive location updates, your app should qualify to be set to run in background mode. Set the "Required background mode" in your plist file.
That will let it fully run in the background and you can get rid of the beginBackgroundTask lines.
The beginBackgroundTaskWithExpirationHandler methods are typically used to request a little extra time for a current task to be completed provided that your app is in the foreground and it moves to the background in the middle of a task. To me it sounds like you want to run in full background mode.
With that said, you should still detected that your program is backgrounded and avoid running unneeded cpu intensive tasks to save battery life.
I need something similar to Facebook's offline post capabilities. Basically I want users to create content locally on the device regardless of connection state, and whenever internet becomes available it should POST/PUT to the server.
I've searched the internet for a solution and I found that NSURLSessionUploadTask can be used for POST-ing in the background. But I couldn't figure out if the following scenarios are supported:
Will my task remain in the background queue when the user is offline and will the operating system try to execute items in the queue upon reconnecting with a network?
What happens if the application is force-closed by the user or crashes?
What happens if the operation fails?
First of all, background NSURLSession allows file upload only. If that is ok for you:
The task will be in the queue until it receives a server answer.
If your app is force-closed, the task will still be executing. When the request is done, your app will be launched in non-interactive background state and receive application:handleEventsForBackgroundURLSession:completionHandler:. After you process the signal and call the completion handler or 30 second timeout, the app will be closed.
I the operation fails, you will receive URLSession:task:didCompleteWithError:
There is a good tutorial on background NSURLSessions. I suggest you to read all 4 parts of this great article.
If file upload is not an option for you, i suggest you to save information into local database and then wait for internet is reachable. (a good approach here is use of Reachability library, Alamofire allows to do that too). When internet becomes available, simply call your http requests with saved data.
We were running into connectivity issues with our internal apps, so we wrote a Swift framework that allows any network operations to be enqueued and sent whenever the device has access to the internet -
https://cocoapods.org/pods/OfflineRequestManager. You'll still have to handle the network request itself within the object conforming to OfflineRequest, but it sounds like a good fit for your use case.
The simplest use case would look something like the following, though most actual cases (saving to disk, specific request data, etc.) will have a few more hoops to jump through:
import OfflineRequestManager
class SimpleRequest: OfflineRequest {
func perform(completion: #escaping (Error?) -> Void) {
doMyNetworkRequest(withCompletion: { response, error in
handleResponse(response)
completion(error)
})
}
}
///////
OfflineRequestManager.defaultManager(queueRequest: SimpleRequest())
Perhaps I have been reading the wrong stuff, but one thing that all of the literatures that I have been reading seem to agree on is that: iOS does not allow background threads to run for longer than ten minutes. That seems to violate one of the greatest principles of app development: the internet should be invisible to your users. So here is a scenario.
A user is going through a tunnel or flying on an airplane, which causes no or unreliable network. At that instant, the user pulls out my email app, composes an email, and hits the send button.
Question: How do I the developer make sure that the email is sent when network becomes available? Of course I am using email as a general example, but in reality I am dealing with a very much simple http situation where my app needs to send a POST to my server.
Side Note: on android, I use Path’s priority job queue, which allows me to set it and forget it (i.e. as soon as there is network it sends my email).
another Side Note: I have been trying to use NSOperationQueue with AFNetworking, but does not do it.
What you want to achieve can be done using a background NSURLSession. While AFNetworking is based on NSURLSession I’m not quite sure if it can be used with a background session that runs while your app doesn’t. But you don’t really need this, NSURLSession is quite easy to use as is.
As a first step you need to create a session configuration for the background session:
let config = URLSessionConfiguration.background(withIdentifier: "de.5sw.test")
config.isDiscretionary = true
config.waitsForConnectivity = true
The isDiscretionary property allows the system to decide when to perform the data transfer. waitsForConnectivity (available since iOS 11) makes the system wait if there is no internet connection instead of failing immediately.
With that configuration object you can create your URL session. The important part is to specify a delegate as the closure-based callbacks get lost when the app is terminated.
let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
To perform your upload you ask the session to create an upload task and then resume it. For the upload task you first create your URLRequest that specifies the URL and all needed headers. The actual data you want to upload needs to be written to a file. If you provide it as a Data or stream object it cannot be uploaded after your app terminates.
let task = session.uploadTask(with: request, fromFile: fileUrl)
task.resume()
To get notified of success or failure of your upload you need to implement the URLSessionDataDelegate method urlSession(_:task:didCompleteWithError:). If error is nil the transfer was successful.
The final piece that is missing is to handle the events that happened while your app was not running. To do this you implement the method application(_:handleEventsForBackgroundURLSession:completionHandler:) in your app delegate. When the system decides that you need to handles some events for background transfers it launches your app in the background and calls this method.
In there you need first store the completion handler and then recreate your URLSession with the same configuration you used before. This then calls it’s delegate for the events you need to handle as usual. Once it is done with the events it calls the delegate method urlSessionDidFinishEvents(forBackgroundURLSession:). From there you need to call the completion handler that was passed to your app delegate.
The session configuration provides some more options:
timeoutIntervalForResource: How long the system should try to perform your upload. Default is 7 days.
sessionSendsLaunchEvents: If false the app will not be launched to handle events. They will be handled when the user opens the app manually. Defaults is true.
Here is a small sample project that shows how everything fits together: https://github.com/5sw/BackgroundUploadDemo
Your app needs to store the data internally and then you either need something which will cause the app to run in the background (but you shouldn't necessarily add something specially if you don't already have a reason to be doing it) or to wait until the user next brings the app to the foreground - then you can check for a network connection and make the call.
Note that e-mail is very different to a POST, because you can pass an e-mail off to the system mail app to send for you but you can't do exactly the same thing with a POST.
Consider looking also at NSURLSessionUploadTask if you can use it.
In three words: you don't.
And that's actually a good thing. I certainly do not want to have to think and speculate about my last 20 apps, if they are still running in the background, using memory and battery and bandwidth. Furthermore, they would be killed if more memory is needed. How would the user be able to predict if it completed its task successfully? He can't, and need to open the app anyhow to check.
As for the email example, I'd go with showing the email as "pending" (i.e. not sent), until it transferred correctly. Make it obvious to the user that he has to come back later to fulfill the job.
While every developer thinks that his app has an extremely good reason for backgrounding, reality is, for the user in 99% it's just a pain. Can you say "task manager"? ;-)
I wrote a pod that does pretty much this - https://cocoapods.org/pods/OfflineRequestManager. You'd have to do some work listening to delegate callbacks if you want to monitor whether the request is in a pending or completed/failed state, but we've been using it to ensure that requests go out in poor or no connectivity scenarios.
The simplest use case would look something like the following, though most actual cases (saving to disk, specific request data, etc.) will have a few more hoops to jump through:
import OfflineRequestManager
class SimpleRequest: OfflineRequest {
func perform(completion: #escaping (Error?) -> Void) {
doMyNetworkRequest(withCompletion: { response, error in
handleResponse(response)
completion(error)
})
}
}
///////
OfflineRequestManager.defaultManager(queueRequest: SimpleRequest())