Swift 4: How to Asynchronously Use Urlsessiondatatask But Have The Requests Be in a Timed Queue

Swift 4: Creating an asynchronous serial queue with 2 seconds wait after each job

If you have

var networkQueue = [NetworkRequest]()
var networkQueueActive = false

Then, your networkRetrieveFromQueue should:

  • check to see if the queue is empty;
  • if not, grab the first item in the queue;
  • initiate the asynchronous request; and
  • in the completion handler of that asynchronous request, call networkRetrieveFromQueue again after 2 seconds

Thus

func startQueue() {
if networkQueueActive { return }

networkQueueActive = true
processNext()
}

// if queue not empty, grab first item, perform request, and call itself
// 2 seconds after prior one finishes

func processNext() {
if networkQueue.isEmpty {
networkQueueActive = false
return
}

let request = networkQueue.removeFirst()

get(request: request) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.processNext()
}
}
}

Where your "process request" might look like:

// perform asynchronous network request, with completion handler that is 
// called when its done

func get(request: NetworkRequest, completionHandler: @escaping () -> Void) {
let task = URLSession.shared.dataTask(with: request.request) { data, _, error in
guard let data = data, error == nil else {
print(error ?? "Unknown error")
completionHandler()
return
}

// process successful response here

// when done, call completion handler

completionHandler()
}
task.resume()
}

Now, I don't know what your NetworkRequest looks like, but this illustrates the basic idea of how to recursively call a function in the completion handler of some asynchronous method.

Limiting the number of fetch requests in URLSession with SwiftUI?

There are several issues:

  1. You are creating a new operation queue every time you call loadImage, rendering the maxConcurrentOperationCount moot. E.g., if you quickly request five images, you will end up with five operation queues, each with one operation on them, and they will run concurrently, with none of the five queues exceeding their respective maxConcurrentOperationCount.

    You must remove the local variable declaration of the operation queue from the function, and make it a property.

  2. DownloadOperation is starting a dataTask but not calling the completion handler. Also, when you create the DownloadOperation you are suppling a completion handler in which you are starting yet another download operation. If you are going to use an Operation to encapsulate the download, you should not have any URLSession code in the completion handler. Use the parameters returned.

  3. The asynchronous operation is not thread-safe. One must synchronize the access to this shared state variable.

Thus, perhaps:

var images: [URLRequest: ImageRequest] = [:]

let queue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
return queue
}()

let session: URLSession = .shared

public func loadImage(_ request: URLRequest) async throws -> UIImage {
switch images[request] {
case .fetched(let image):
return image

case .inProgress(let task):
return try await task.value

case .failure(let error):
throw error

case nil:
let task: Task<UIImage, Error> = Task {
try await withCheckedThrowingContinuation { continuation in
let operation = ImageRequestOperation(session: session, request: request) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .failure(let error):
self?.images[request] = .failure(error)
continuation.resume(throwing: error)

case .success(let image):
self?.images[request] = .fetched(image)
continuation.resume(returning: image)
}
}
}

queue.addOperation(operation)
}
}

images[request] = .inProgress(task)

return try await task.value
}
}

Where the above, the async-await code, uses the following operation:

class ImageRequestOperation: DataRequestOperation {
init(session: URLSession, request: URLRequest, completionHandler: @escaping (Result<UIImage, Error>) -> Void) {
super.init(session: session, request: request) { result in
switch result {
case .failure(let error):
DispatchQueue.main.async { completionHandler(.failure(error)) }

case .success(let data):
guard let image = UIImage(data: data) else {
DispatchQueue.main.async { completionHandler(.failure(URLError(.badServerResponse))) }
return
}

DispatchQueue.main.async { completionHandler(.success(image)) }
}
}
}
}

The the above abstracts the image-related part, above, from the network-related stuff, below. Thus:

class DataRequestOperation: AsynchronousOperation {
private var task: URLSessionDataTask!

init(session: URLSession, request: URLRequest, completionHandler: @escaping (Result<Data, Error>) -> Void) {
super.init()

task = session.dataTask(with: request) { data, response, error in
guard
let data = data,
let response = response as? HTTPURLResponse,
200 ..< 300 ~= response.statusCode
else {
completionHandler(.failure(error ?? URLError(.badServerResponse)))
return
}

completionHandler(.success(data))

self.finish()
}
}

override func main() {
task.resume()
}

override func cancel() {
super.cancel()

task.cancel()
}
}

And the above inherits from an AsynchronousOperation that abstracts all of your asynchronous operation stuff, below, from the substance of what the operation does, above. Thus:

/// AsynchronousOperation
///
/// Encapsulate the basic asynchronous operation logic in its own class, to avoid cluttering
/// your concrete implementations with a ton of boilerplate code.

class AsynchronousOperation: Operation {
enum OperationState: Int {
case ready
case executing
case finished
}

@Atomic var state: OperationState = .ready {
willSet {
willChangeValue(forKey: #keyPath(isExecuting))
willChangeValue(forKey: #keyPath(isFinished))
}

didSet {
didChangeValue(forKey: #keyPath(isFinished))
didChangeValue(forKey: #keyPath(isExecuting))
}
}

override var isReady: Bool { state == .ready && super.isReady }
override var isExecuting: Bool { state == .executing }
override var isFinished: Bool { state == .finished }
override var isAsynchronous: Bool { true }

override func start() {
if isCancelled {
state = .finished
return
}

state = .executing

main()
}

/// Subclasses should override this method, but *not* call this `super` rendition.

override func main() {
assertionFailure("The `main` method should be overridden in concrete subclasses of this abstract class.")
}

func finish() {
state = .finished
}
}

And, note, that I addressed the lack of thread-safe access to the state using this property wrapper:

/// Atomic
///
/// Property wrapper providing atomic interface.
///
/// - Note: It is advised to use this with value types only. If you use reference types, the object could theoretically be mutated beyone the knowledge of this property wrapper, losing atomic behavior.

@propertyWrapper
struct Atomic<T> {
var _wrappedValue: T
let lock = NSLock()

var wrappedValue: T {
get { synchronized { _wrappedValue } }
set { synchronized { _wrappedValue = newValue } }
}

init(wrappedValue: T) {
_wrappedValue = wrappedValue
}

func synchronized<T>(block: () throws -> T) rethrows -> T {
lock.lock()
defer { lock.unlock() }
return try block()
}
}

This yields asynchronous behaviors with max concurrency count of 3. E.g., here I download 10 images, then another 10, and then another 20:

Sample Image

How to Find which response belongs to which request in Async api calling using URLSession

You have many ways to achieve this.

1 - Basically, if you call your client function with an identifier, you will be able to retrieve it in your completion block:

func call(with identifier: String, at url: URL) {
URLSession.shared.dataTask(url: url) { (_, _, _) in
print(identifier)
}.resume()
}

2 - You can also use the taskIdentifier of an URLSessionDataTask. But to do this, you will need to use the delegate of your custom URLSession:

self.session = URLSession(configuration: URLSessionConfiguration.default,
delegate: self,
delegateQueue: nil)

then you will not use a completion block but the delegate function instead:

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
print(dataTask.taskIdentifier)
}

(of course you need to know which task identifier has been set for which URLSessionDataTask)

3 - If you need to access your identifier from your completion block, you can write a function which will happened it in the list of the parameter of the default completion block:

func dataTask(session: URLSession,
url: URL,
identifier: String,
completionBlock: @escaping (String, Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
return session.dataTask(with: url) { (data, response, error) in
completionBlock(identifier, data, response, error)
}
}

4 - If you need to have a custom identifier in a URLSessionDataTask object, you can add it using extension and associated object:

extension URLSessionDataTask {
var identifier: String? {
get {
let identifier = objc_getAssociatedObject(self, &kIdentiferId)
if let id = identifier as? String {
return id
} else {
return nil
}
}
set {
objc_setAssociatedObject(self, &kIdentiferId, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}
}

private var kIdentiferId: Int8 = 100

Then you can use it like this:

let task = session.dataTask(url: url)
task.identifier = "hello"

Can I use URLSession in a function with return?

You cant return because a network requests happen asychronously. Instead of using a return, use a closure that is called when the request has completed. Check out the following:

func sendRequest(_ _request:URLRequest, completion:@escaping (_ success:Bool,_ error:Error?, _ _data:[String:AnyObject]?)->Void) -> URLSessionDataTask {
let session:URLSession = URLSession(configuration: URLSessionConfiguration.default, delegate: nil, delegateQueue: self.queue)
let task:URLSessionDataTask = session.dataTask(with: _request) { (_data, _response, _error) in
if let resultsDic:[String:AnyObject] = self.parseJSONFromData(retrievedData, withInitalKey: self.getInitialKey()){
completion(true, nil, resultsDic)
} else {
completion(false, nil, nil)
}
}
task.resume()
return task
}

NSURLSession: How to increase time out for URL requests?

ObjC

NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 30.0;
sessionConfig.timeoutIntervalForResource = 60.0;

Swift

let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = 30.0
sessionConfig.timeoutIntervalForResource = 60.0
let session = URLSession(configuration: sessionConfig)

What docs say

timeoutIntervalForRequest and timeoutIntervalForResource specify the timeout interval for the request as well as the resource.

timeoutIntervalForRequest - The timeout interval to use when waiting
for additional data. The timer associated with this value is reset
whenever new data arrives. When the request timer reaches the
specified interval without receiving any new data, it triggers a
timeout.

timeoutIntervalForResource - The maximum amount of time that a
resource request should be allowed to take. This value controls how
long to wait for an entire resource to transfer before giving up. The
resource timer starts when the request is initiated and counts until
either the request completes or this timeout interval is reached,
whichever comes first.

Based on NSURLSessionConfiguration Class Reference



Related Topics



Leave a reply



Submit