Swift Background Execution - ios

I have a step function on AWS triggered by an HTTP Post request. The function can take a few seconds to complete. I'd like for execution to continue if the user puts the app into the background, and to correctly navigate to the next screen once the user puts the app back into the foreground (if execution has finished).
My API Client endpoint looks like this:
func connect<OutputType: Decodable>(to request: URLRequestConvertible, decoder: JSONDecoder) -> AnyPublisher<Result<OutputType, Error>, Never> {
var request = request.asURLRequest()
if let token: String = KeychainWrapper.standard.string(forKey: "apiToken") {
request.addValue(token, forHTTPHeaderField: "Authorization")
}
let configuration = URLSessionConfiguration.default
configuration.waitsForConnectivity = true
let session = URLSession(configuration: configuration)
return session.dataTaskPublisher(for: request)
.tryMap({ (data, response) -> Data in
guard let response = response as? HTTPURLResponse else { throw NetworkError.invalidResponse }
guard 200..<300 ~= response.statusCode else {
throw NetworkError.invalidStatusCode(statusCode: response.statusCode)
}
return data
})
.decode(type: OutputType.self, decoder: decoder)
.map(Result.success)
.catch { error -> Just<Result<OutputType, Error>> in Just(.failure(error)) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
I'd like to know the best practice for implementing this call. I'm currently using beginBackgroundTask below.
func makeRequest() {
DispatchQueue.global(qos: .userInitiated).async {
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "Request Name") {
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = .invalid
}
<implementation>
}
}
However, implementation only works if I have nested DispatchQueue.main.async blocks where I perform more logic after making the HTTP request (like determining which screen to navigate to next after we receive the response.
Is this the best way to do it? Is it ok to have a few different nested DispatchQueue.main.async blocks inside the DispatchQueue.global block? Should I post the .receive(on: ) to DispatchQueue.global?

You don’t have to dispatch this background task to a background queue at all. (Don’t conflate the “background task”, which refers to the app state, with the “background queue”, which governs which threads are used.)
Besides, as the documentation says, the expiration handler closure runs on the main thread:
The system calls the handler synchronously on the main thread, blocking the app’s suspension momentarily.
So you really want to keep all interaction with backgroundTaskID on the main thread, anyway, or else you would have to implement some other synchronization mechanism.
And as a matter of good practice, make sure to end your background task when your asynchronous request is done (rather than relying on the expiration/timeout closure).

Related

Cancelling an async/await Network Request

I have a networking layer that currently uses completion handlers to deliver a result on the operation is complete.
As I support a number of iOS versions, I instead extend the network layer within the app to provide support for Combine. I'd like to extend this to now also a support Async/Await but I am struggling to understand how I can achieve this in a way that allows me to cancel requests.
The basic implementation looks like;
protocol HTTPClientTask {
func cancel()
}
protocol HTTPClient {
typealias Result = Swift.Result<(data: Data, response: HTTPURLResponse), Error>
#discardableResult
func dispatch(_ request: URLRequest, completion: #escaping (Result) -> Void) -> HTTPClientTask
}
final class URLSessionHTTPClient: HTTPClient {
private let session: URLSession
init(session: URLSession) {
self.session = session
}
func dispatch(_ request: URLRequest, completion: #escaping (HTTPClient.Result) -> Void) -> HTTPClientTask {
let task = session.dataTask(with: request) { data, response, error in
completion(Result {
if let error = error {
throw error
} else if let data = data, let response = response as? HTTPURLResponse {
return (data, response)
} else {
throw UnexpectedValuesRepresentation()
}
})
}
task.resume()
return URLSessionTaskWrapper(wrapped: task)
}
}
private extension URLSessionHTTPClient {
struct UnexpectedValuesRepresentation: Error {}
struct URLSessionTaskWrapper: HTTPClientTask {
let wrapped: URLSessionTask
func cancel() {
wrapped.cancel()
}
}
}
It very simply provides an abstraction that allows me to inject a URLSession instance.
By returning HTTPClientTask I can call cancel from a client and end the request.
I extend this in a client app using Combine as follows;
extension HTTPClient {
typealias Publisher = AnyPublisher<(data: Data, response: HTTPURLResponse), Error>
func dispatchPublisher(for request: URLRequest) -> Publisher {
var task: HTTPClientTask?
return Deferred {
Future { completion in
task = self.dispatch(request, completion: completion)
}
}
.handleEvents(receiveCancel: { task?.cancel() })
.eraseToAnyPublisher()
}
}
As you can see I now have an interface that supports canceling tasks.
Using async/await however, I am unsure what this should look like, how I can provide a mechanism for canceling requests.
My current attempt is;
extension HTTPClient {
func dispatch(_ request: URLRequest) async -> HTTPClient.Result {
let task = Task { () -> (data: Data, response: HTTPURLResponse) in
return try await withCheckedThrowingContinuation { continuation in
self.dispatch(request) { result in
switch result {
case let .success(values): continuation.resume(returning: values)
case let .failure(error): continuation.resume(throwing: error)
}
}
}
}
do {
let output = try await task.value
return .success(output)
} catch {
return .failure(error)
}
}
}
However this simply provides the async implementation, I am unable to cancel this.
How should this be handled?
Swift’s new concurrency model handles cancellation perfectly well. While the WWDC 2021 videos focused on the checkCancellation and isCancelled patterns (e.g., the Explore structured concurrency in Swift video), in this case, one would use withTaskCancellationHandler to create a task that cancels the network request when the task, itself, is canceled. (Obviously, this is only a concern in iOS 13/14, as in iOS 15 one would just use the provided async methods, data(for:delegate) or data(from:delegate:), which also handle cancelation well.)
See SE-0300: Continuations for interfacing async tasks with synchronous code: Additional Examples for example. That download example is a bit outdated, so here is an updated rendition:
extension URLSession {
#available(iOS, deprecated: 15, message: "Use `data(from:delegate:)` instead")
#available(macOS, deprecated: 12, message: "Use `data(from:delegate:)` instead")
func data(with url: URL) async throws -> (URL, URLResponse) {
try await download(with: URLRequest(url: url))
}
#available(iOS, deprecated: 15, message: "Use `data(for:delegate:)` instead")
#available(macOS, deprecated: 12, message: "Use `data(for:delegate:)` instead")
func data(with request: URLRequest) async throws -> (Data, URLResponse) {
let sessionTask = SessionTask(session: self)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.data(for: request) { data, response, error in
guard let data, let response else {
continuation.resume(throwing: error ?? URLError(.badServerResponse))
return
}
continuation.resume(returning: (data, response))
}
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
}
private extension URLSession {
actor SessionTask {
var state: State = .ready
private let session: URLSession
init(session: URLSession) {
self.session = session
}
func cancel() {
if case .executing(let task) = state {
task.cancel()
}
state = .cancelled
}
}
}
// MARK: Data
extension URLSession.SessionTask {
func data(for request: URLRequest, completionHandler: #Sendable #escaping (Data?, URLResponse?, Error?) -> Void) {
if case .cancelled = state {
completionHandler(nil, nil, CancellationError())
return
}
let task = session.dataTask(with: request, completionHandler: completionHandler)
state = .executing(task)
task.resume()
}
}
extension URLSession.SessionTask {
enum State {
case ready
case executing(URLSessionTask)
case cancelled
}
}
A few minor observations on my code snippet:
I gave these names to avoid collision with the iOS 15 method names, but added deprecated messages to inform the developer to use the iOS 15 renditions once you abandon iOS 13/14 support.
I deviated from SE-0300’s example to follow the pattern of the data(from:delegate:) and data(for:delegate:) methods (returning a tuple with Data and a URLResponse).
The actor, not in the original example, is needed to synchronize the access to the URLSessionTask.
Note that according to SE-0304, that regarding withTaskCancellationHandler:
If the task has already been cancelled at the point withTaskCancellationHandler is called, the cancellation handler is invoked immediately, before the operation block is executed.
Because of this, the actor in the above uses a state variable to determine if the request has already been canceled, and just immediately resumes, throwing a CancellationError if it is already canceled.
But all of that is unrelated to the question at hand. In short, use withTaskCancellationHandler.
E.g. Here are five image requests that I started in a task group, as monitored by Charles:
And here are the same requests, but this time I canceled the whole task group (and the cancelations successfully stopped the associated network requests for me):
(Obviously the x-axis scale is different.)
If you need download renditions (to wrap downloadTask), you could do supplement the above with:
extension URLSession {
#available(iOS, deprecated: 15, message: "Use `download(from:delegate:)` instead")
#available(macOS, deprecated: 12, message: "Use `download(from:delegate:)` instead")
func download(with url: URL) async throws -> (URL, URLResponse) {
try await download(with: URLRequest(url: url))
}
#available(iOS, deprecated: 15, message: "Use `download(for:delegate:)` instead")
#available(macOS, deprecated: 12, message: "Use `download(for:delegate:)` instead")
func download(with request: URLRequest) async throws -> (URL, URLResponse) {
let sessionTask = SessionTask(session: self)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.download(for: request) { location, response, error in
guard let location, let response else {
continuation.resume(throwing: error ?? URLError(.badServerResponse))
return
}
// since continuation can happen later, let’s figure out where to store it ...
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension(request.url!.pathExtension)
// ... and move it to there
do {
try FileManager.default.moveItem(at: location, to: tempURL)
} catch {
continuation.resume(throwing: error)
return
}
continuation.resume(returning: (tempURL, response))
}
}
}
} onCancel: {
Task { await sessionTask.cancel() }
}
}
}
extension URLSession.SessionTask {
func download(for request: URLRequest, completionHandler: #Sendable #escaping (URL?, URLResponse?, Error?) -> Void) {
if case .cancelled = state {
completionHandler(nil, nil, CancellationError())
return
}
let task = session.downloadTask(with: request, completionHandler: completionHandler)
state = .executing(task)
task.resume()
}
}
You can't hybridize Combine with async/await. If you embrace async/await fully and call one of the async download methods...
https://developer.apple.com/documentation/foundation/urlsession/3767353-data
...then the task where you call that method will be cancellable in good order through the standard structured concurrency mechanism.
So if you want to support Swift 5.5 / iOS 15 async and yet support earlier versions too, you will need two completely independent implementations of this functionality.
async/await might not be the proper paradigm if you want cancellation. The reason is that the new structured concurrency support in Swift allows you to write code that looks single-threaded/synchronous, but it fact it's multi-threaded.
Take for example a naive synchronous code:
let data = tryData(contentsOf: fileURL)
If the file is huge, then it might take a lot of time for the operation to finish, and during this time the operation cannot be cancelled, and the caller thread is blocked.
Now, assuming Data exports an async version of the above initializer, you'd write the async version of the code similar to this:
let data = try await Data(contentsOf: fileURL)
For the developer, it's the same coding style, once the operation finishes, they'll either have a data variable to use, or they'll be receiving an error.
In both cases, there's no cancellation built in, as the operation is synchronous from the developer's perspective. The major difference is that the await-ed call doesn't block the caller thread, but on the other hand once the control flow returns it might well be that the code continues executing on a different thread.
Now, if you need support for cancellation, then you'll have to store somewhere some identifiable data that can be used to cancel the operation.
If you'll want to store those identifiers from the caller scope, then you'll need to split your operation in two: initialization, and execution.
Something along the lines of
extension HTTPClient {
// note that this is not async
func task(for request: URLRequest) -> HTTPClientTask {
// ...
}
}
class HTTPClientTask {
func dispatch() async -> HTTPClient.Result {
// ...
}
}
let task = httpClient.task(for: urlRequest)
self.theTask = task
let result = await task.dispatch()
// somewhere outside the await scope
self.theTask.cancel()

DispatchQueue vs Delegates vs Closures in Swift

Excuse me if this a noobish question but I don't know the difference between executing a block of code after an API request is received and parsed via GCD, delegates and closures.
As far as I know, a creating a session to download data from an API URL is done on the main thread unless I execute the code inside a a GCD block or a delegate or a closure.
Here are two examples:
Using GCD
DispatchQueue.global(qos: .utility).async {
let requestURL = URL(string: "http://echo.jsontest.com/key/value/one/two")
let session = URLSession.shared
let task = session.dataTask(with: requestURL!) {
(data, response, error) in
print(data as Any)
DispatchQueue.main.async {
print("Hello")
}
}
task.resume()
}
Using Delegate:
import Foundation
import UIKit
protocol WeatherDataDownloaderProtocol {
func setData(weatherData: WeatherData)
}
class WeatherDataDownloader {
var weatherData = WeatherData()
var delegate: WeatherDataDownloaderProtocol?
func downloadWeatherData() {
let API_URL = WEATHER_FORECAST_URL
guard let URL = URL(string: API_URL) else {
print("Error: No valid URL")
return
}
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: URL) { (data, response, error) in
guard error == nil else {
print("Error getting data")
print("\(error)")
return
}
guard let responseData = data else {
print("Error: Did not receive data")
return
}
do {
guard let JSON = try JSONSerialization.jsonObject(with: responseData, options: []) as? Dictionary<String, AnyObject> else {
print("Error: Error trying to convert data to JSON")
return
}
print(JSON)
self.sendDataBack()
} catch {
print("Error: Parsing JSON data error")
return
}
}
task.resume()
}
func sendDataBack() {
if let _delegate = delegate {
_delegate.setData(weatherData: weatherData)
}
}
}
Both, print("Hello") and print(JSON) + self.sendDataBack() will execute after the JSON is retrieved and parsed. What's the difference between both methods? Does it have anything to do with whether my app would crash if I navigate out of the viewController while waiting for the network response?
Thanks a lot
In your first approach, the .async call is not necessary. URLSession dataTask is a background task.
So the choice is not GDC vs. delegates but completion handler vs. delegate.
Opinion based:
Using a delegate is more work and harder to read because you have to check in other areas of the code if the delegate is actually set and who it is and what it actually does.
Also no code might be executed in case the delegate does not exist any more at the time your network call has finished. So for this case I plead for using a completion closure.
Both are correct. The block/closure approach is newer and considered to have better readability since you don't have to jump between functions and even between files to follow the course of your code.
DispatchQueue.global(qos: .utility).async {
let requestURL = URL(string: "http://echo.jsontest.com/key/value/one/two")
let session = URLSession.shared
let task = session.dataTask(with: requestURL!) {
(data, response, error) in
print(data as Any)
DispatchQueue.main.async {
print("Hello")
}
}
task.resume()
}
In this method your service hits in background thread and when you completed your in background thread you come back in main thread using this method
DispatchQueue.main.async {
print("Hello")
}
and then your print("Hello") will call in main thread.
While the method
downloadWeatherData
defined in appdelegate also hits the service in background thread but in the manner of closure because closure also works like a background thread. Using closure when your task completes your control automatically comes back in main thread where you call print(JSON).
Now comes to your problem, the best thing is that you should wait untill your task complete and you get the json response on your viewcontroller then move to your next controller other your app may crash in some situations.

Swift, dispatch_group_wait not waiting

I am trying to use grand central dispatch to wait for files to finish download before continuing. This question is a spin-off from this one: Swift (iOS), waiting for all images to finish downloading before returning.
I am simply trying to find out how to get dispatch_group_wait (or similar) to actually wait and not just continue before the downloads have finished. Note that if I use NSThread.sleepForTimeInterval instead of calling downloadImage, it waits just fine.
What am I missing?
class ImageDownloader {
var updateResult = AdUpdateResult()
private let fileManager = NSFileManager.defaultManager()
private let imageDirectoryURL = NSURL(fileURLWithPath: Settings.adDirectory, isDirectory: true)
private let group = dispatch_group_create()
private let downloadQueue = dispatch_queue_create("com.acme.downloader", DISPATCH_QUEUE_SERIAL)
func downloadImages(imageFilesOnServer: [AdFileInfo]) {
dispatch_group_async(group, downloadQueue) {
for serverFile in imageFilesOnServer {
print("Start downloading \(serverFile.fileName)")
//NSThread.sleepForTimeInterval(3) // Using a sleep instead of calling downloadImage makes the dispatch_group_wait below work
self.downloadImage(serverFile)
}
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // This does not wait for downloads to finish. Why?
print("All Done!") // It gets here too early!
}
private func downloadImage(serverFile: AdFileInfo) {
let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)
Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
.response { _, _, _, error in
if let error = error {
print("Error downloading \(serverFile.fileName): \(error)")
} else {
self.updateResult.filesDownloaded++
print("Done downloading \(serverFile.fileName)")
}
}
}
}
Note: these downloads are in response to an HTTP POST request and I am using an HTTP server (Swifter) which does not support asynchronous operations, so I do need to wait for the full downloads to complete before returning a response (see original question referenced above for more details).
When using dispatch_group_async to call methods that are, themselves, asynchronous, the group will finish as soon as all of the asynchronous tasks have started, but will not wait for them to finish. Instead, you can manually call dispatch_group_enter before you make the asynchronous call, and then call dispatch_group_leave when the asynchronous call finish. Then dispatch_group_wait will now behave as expected.
To accomplish this, though, first change downloadImage to include completion handler parameter:
private func downloadImage(serverFile: AdFileInfo, completionHandler: (NSError?)->()) {
let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)
Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
.response { _, _, _, error in
if let error = error {
print("Error downloading \(serverFile.fileName): \(error)")
} else {
print("Done downloading \(serverFile.fileName)")
}
completionHandler(error)
}
}
I've made that a completion handler that passes back the error code. Tweak that as you see fit, but hopefully it illustrates the idea.
But, having provided the completion handler, now, when you do the downloads, you can create a group, "enter" the group before you initiate each download, "leave" the group when the completion handler is called asynchronously.
But dispatch_group_wait can deadlock if you're not careful, can block the UI if done from the main thread, etc. Better, you can use dispatch_group_notify to achieve the desired behavior.
func downloadImages(_ imageFilesOnServer: [AdFileInfo], completionHandler: #escaping (Int) -> ()) {
let group = DispatchGroup()
var downloaded = 0
group.notify(queue: .main) {
completionHandler(downloaded)
}
for serverFile in imageFilesOnServer {
group.enter()
print("Start downloading \(serverFile.fileName)")
downloadImage(serverFile) { error in
defer { group.leave() }
if error == nil {
downloaded += 1
}
}
}
}
And you'd call it like so:
downloadImages(arrayOfAdFileInfo) { downloaded in
// initiate whatever you want when the downloads are done
print("All Done! \(downloaded) downloaded successfully.")
}
// but don't do anything contingent upon the downloading of the images here
For Swift 2 and Alamofire 3 answer, see previous revision of this answer.
In Swift 3...
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
// do something, including background threads
dispatchGroup.leave()
dispatchGroup.notify(queue: DispatchQueue.main) {
// completion code
}
https://developer.apple.com/reference/dispatch/dispatchgroup
The code is doing exactly what you are telling it to.
The call to dispatch_group_wait will block until the block inside the call to dispatch_group_async is finished.
The block inside the call to dispatch_group_async will be finished when the for loop completes. This will complete almost immediately since the bulk of the work being done inside the downloadImage function is being done asynchronously.
This means the for loop finishes very quickly and that block is done (and dispatch_group_wait stops waiting) long before any of the actual downloads are completed.
I would make use of dispatch_group_enter and dispatch_group_leave instead of dispatch_group_async.
I would change your code to something like the following (not tested, could be typos):
class ImageDownloader {
var updateResult = AdUpdateResult()
private let fileManager = NSFileManager.defaultManager()
private let imageDirectoryURL = NSURL(fileURLWithPath: Settings.adDirectory, isDirectory: true)
private let group = dispatch_group_create()
private let downloadQueue = dispatch_queue_create("com.acme.downloader", DISPATCH_QUEUE_SERIAL)
func downloadImages(imageFilesOnServer: [AdFileInfo]) {
dispatch_async(downloadQueue) {
for serverFile in imageFilesOnServer {
print("Start downloading \(serverFile.fileName)")
//NSThread.sleepForTimeInterval(3) // Using a sleep instead of calling downloadImage makes the dispatch_group_wait below work
self.downloadImage(serverFile)
}
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // This does not wait for downloads to finish. Why?
print("All Done!") // It gets here too early!
}
private func downloadImage(serverFile: AdFileInfo) {
dispatch_group_enter(group);
let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)
Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
.response { _, _, _, error in
if let error = error {
print("Error downloading \(serverFile.fileName): \(error)")
} else {
self.updateResult.filesDownloaded++
print("Done downloading \(serverFile.fileName)")
}
dispatch_group_leave(group);
}
}
}
This change should do what you need. Each call to downloadImage enters the group and it doesn't leave the group until the download completion handler is called.
Using this pattern, the final line will execute when the other tasks are finished.
let group = dispatch_group_create()
dispatch_group_enter(group)
// do something, including background threads
dispatch_group_leave(group) // can be called on a background thread
dispatch_group_enter(group)
// so something
dispatch_group_leave(group)
dispatch_group_notify(group, mainQueue) {
// completion code
}

Setting a timeout for a request method

I'm trying to set up a timeout for a request method that checks username availability. When the user types in a username and presses a button, the checkUsername method is called. My code is not working because the code inside Timeout(5.0){} is never executed and timeout never gets the value false. I know this is not the best way to do it but I wanted to give it a try and wonder if this can be modified in some way or do I need a different approach?
var timeout: Bool = false
func usernameAvailable(username: String) -> String{
let response: String!
response = Server.checkUsername(username!)
Timeout(5.0){
self.timeout = true
}
while(!timeout){
if(response != nil){
return response
}
}
return "Timeout"
}
The Timeout.swift class looks like this and is working
class Timeout: NSObject{
private var timer: NSTimer?
private var callback: (Void -> Void)?
init(_ delaySeconds: Double, _ callback: Void -> Void){
super.init()
self.callback = callback
self.timer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(delaySeconds),
target: self, selector: "invoke", userInfo: nil, repeats: false)
}
func invoke(){
self.callback?()
// Discard callback and timer.
self.callback = nil
self.timer = nil
}
func cancel(){
self.timer?.invalidate()
self.timer = nil
}
}
I see what you are trying to do and it would make more sense to use an existing framework unless you really need/want to write your own networking code.
I would suggest instead to use the timeoutInterval support in an NSURLRequest along with a completion handler on NSURLSession to achieve the solution that you are seeking.
A timeout of the server response can be handled in the completion handler of something like an NSURLSessionDataTask.
Here is a working example to help get you started that retrieves data from the iTunes Store to illustrate how your timeout could be handled:
let timeout = 5 as NSTimeInterval
let searchTerm = "philip+glass"
let url = NSURL(string: "https://itunes.apple.com/search?term=\(searchTerm)")
let request: NSURLRequest = NSURLRequest(URL: url!,
cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringCacheData,
timeoutInterval: timeout)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task: NSURLSessionDataTask = session.dataTaskWithRequest(request, completionHandler: {
(data, response, error) in
if response == nil {
print("Timeout")
} else {
print(String(data: data!, encoding: NSUTF8StringEncoding))
}
}
)
task.resume()
If you reduce the timeout interval to something short, you can force the timeout to happen.
The code in the Timeout block will never run because the timer will fire on the on the main thread, but you're blocking the main thread with your while loop.
You have another issue here, that you're calling Server.checkUsername(username!) and returning that result, which would suggest that this must be a synchronous call (which is not good). So, this is also likely blocking the main thread there. It won't even try to start the Timeout logic until checkUsername returns.
There are kludgy fixes for this, but in my opinion, this begs for a very different pattern. One should never write code that has a spinning while loop that is polling some completion status. It is much better to adopt asynchronous patterns with completionHandler closures. But without more information on what checkUsername is doing, it's hard to get more specific.
But, ideally, if your checkUsername is building a NSMutableURLRequest, just specify timeoutInterval for that and then have the NSURLSessionTask completion block check for NSError with domain of NSURLErrorDomain and a code of NSURLError.TimedOut. You also probably want to cancel the prior request if it's already running.
func startRequestForUsername(username: String, timeout: NSTimeInterval, completionHandler: (Bool?, NSError?) -> ()) -> NSURLSessionTask {
let request = NSMutableURLRequest(URL: ...) // configure your request however appropriate for your web service
request.timeoutInterval = timeout // but make sure to specify timeout
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
dispatch_async(dispatch_get_main_queue()) {
guard data != nil && error == nil else {
completionHandler(nil, error)
return
}
let usernameAvailable = ... // parse the boolean success/failure out of the `data` however appropriate
completionHandler(usernameAvailable, nil)
}
}
task.resume()
return task
}
And you can then use it like so:
private weak var previousTask: NSURLSessionTask?
func checkUsername(username: String) {
// update the UI to say that we're checking the availability of the user name here, e.g.
usernameStatus.text = "Checking username availability..."
// now, cancel prior request (if any)
previousTask?.cancel()
// start new request
let task = startRequestForUsername(username, timeout: 5) { usernameAvailable, error in
guard usernameAvailable != nil && error == nil else {
if error?.domain == NSURLErrorDomain && error?.code == NSURLError.TimedOut.rawValue {
// everything is cool, the task just timed out
} else if error?.domain == NSURLErrorDomain && error?.code != NSURLError.Cancelled.rawValue {
// again, everything is cool, the task was cancelled
} else {
// some error other happened, so handle that as you see fit
// but the key issue that if it was `.TimedOut` or `.Cancelled`, then don't do anything
}
return
}
if usernameAvailable! {
// update UI to say that the username is available
self.usernameStatus.text = "Username is available"
} else {
// update UI to say that the username is not available
self.usernameStatus.text = "Username is NOT available"
}
}
// save reference to this task
previousTask = task
}
By the way, if you do this sort of graceful, asynchronous processing of requests, you can also increase the timeout interval (e.g. maybe 10 or 15 seconds). We're not freezing the UI, so we can do whatever we want, and not artificially constrain the time allowed for the request.

how to make alamofire download progress run in background ios?

I am using Alamofire to download data
How to make alamofire run download in background with swift?
Thanks
The basic idea is as follows:
The key problem is that with background downloads, your app may actually be terminated while downloads are in progress (e.g. jettisoned due to memory pressure). Fortunately, your app is fired up again when background downloads are done, but any task-level closures you originally supplied are long gone. To get around this, when using background sessions, one should rely upon session-level closures used by the delegate methods.
import UIKit
import Alamofire
import UserNotifications
fileprivate let backgroundIdentifier = ...
fileprivate let notificationIdentifier = ...
final class BackgroundSession {
/// Shared singleton instance of BackgroundSession
static let shared = BackgroundSession()
/// AlamoFire `SessionManager`
///
/// This is `private` to keep this app loosely coupled with Alamofire.
private let manager: SessionManager
/// Save background completion handler, supplied by app delegate
func saveBackgroundCompletionHandler(_ backgroundCompletionHandler: #escaping () -> Void) {
manager.backgroundCompletionHandler = backgroundCompletionHandler
}
/// Initialize background session
///
/// This is `private` to avoid accidentally instantiating separate instance of this singleton object.
private init() {
let configuration = URLSessionConfiguration.background(withIdentifier: backgroundIdentifier)
manager = SessionManager(configuration: configuration)
// specify what to do when download is done
manager.delegate.downloadTaskDidFinishDownloadingToURL = { _, task, location in
do {
let destination = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent(task.originalRequest!.url!.lastPathComponent)
try FileManager.default.moveItem(at: location, to: destination)
} catch {
print("\(error)")
}
}
// specify what to do when background session finishes; i.e. make sure to call saved completion handler
// if you don't implement this, it will call the saved `backgroundCompletionHandler` for you
manager.delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] _ in
self?.manager.backgroundCompletionHandler?()
self?.manager.backgroundCompletionHandler = nil
// if you want, tell the user that the downloads are done
let content = UNMutableNotificationContent()
content.title = "All downloads done"
content.body = "Whoo, hoo!"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let notification = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(notification)
}
// specify what to do upon error
manager.delegate.taskDidComplete = { _, task, error in
let filename = task.originalRequest!.url!.lastPathComponent
if let error = error {
print("\(filename) error: \(error)")
} else {
print("\(filename) done!")
}
// I might want to post some event to `NotificationCenter`
// so app UI can be updated, if it's in foreground
}
}
func download(_ url: URL) {
manager.download(url)
}
}
Then I can just initiate those downloads. Note, I do not specify any task-specific closure when I initiate the download, but rather merely use the above session-level closures that use the details of the URLSessionTask to identify what to do:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// request permission to post notification if download finishes while this is running in background
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
if let error = error, !granted {
print("\(error)")
}
}
}
#IBAction func didTapButton(_ sender: Any) {
let urlStrings = [
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/s72-55482.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo10/hires/as10-34-5162.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo-soyuz/apollo-soyuz/hires/s75-33375.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-134-20380.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-140-21497.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-148-22727.jpg"
]
let urls = urlStrings.flatMap { URL(string: $0) }
for url in urls {
BackgroundSession.shared.download(url)
}
}
}
If your app isn't running when the downloads finish, iOS needs to know that, after it restarted your app, when you're all done and that it can safely suspend your app. So, in handleEventsForBackgroundURLSession you capture that closure:
class AppDelegate: UIResponder, UIApplicationDelegate {
...
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
BackgroundSession.shared.saveBackgroundCompletionHandler(completionHandler)
}
}
That is used by sessionDidFinishEventsForBackgroundURLSession, in step 1.
Two observations:
This is only called if your app was not running when the downloads finish.
If doing background sessions, though, you must capture this closure and call it when you're all done processing the background session delegate methods.
So, to recap, the basic limitations of background sessions are:
You can only use download and upload tasks while the app is in background;
You can only rely upon session-level delegates because the app may have been terminated since the requests were initiated; and
In iOS, you must implement handleEventsForBackgroundURLSession, capture that completion handler, and call it when your background process is done.
I must also point out that while Alamofire is a wonderful library, it's not actually adding a lot value (above and beyond what is provided by URLSession to this background download process). If you're doing simple uploads/downloads only, then you might consider just using URLSession directly. But if you are using Alamofire in your project already or if your requests consist of more complicated application/x-www-form-urlencoded requests (or whatever) which merit the advantages of Alamofire, then the above outlines the key moving parts involved in the process.

Resources