NSURLSession concurrent requests with Alamofire
Yes, this is expected behavior. One solution is to wrap your requests in custom, asynchronous NSOperation
subclass, and then use the maxConcurrentOperationCount
of the operation queue to control the number of concurrent requests rather than the HTTPMaximumConnectionsPerHost
parameter.
The original AFNetworking did a wonderful job wrapping the requests in operations, which made this trivial. But AFNetworking's NSURLSession
implementation never did this, nor does Alamofire.
You can easily wrap the Request
in an NSOperation
subclass. For example:
class NetworkOperation: AsynchronousOperation {
// define properties to hold everything that you'll supply when you instantiate
// this object and will be used when the request finally starts
//
// in this example, I'll keep track of (a) URL; and (b) closure to call when request is done
private let urlString: String
private var networkOperationCompletionHandler: ((_ responseObject: Any?, _ error: Error?) -> Void)?
// we'll also keep track of the resulting request operation in case we need to cancel it later
weak var request: Alamofire.Request?
// define init method that captures all of the properties to be used when issuing the request
init(urlString: String, networkOperationCompletionHandler: ((_ responseObject: Any?, _ error: Error?) -> Void)? = nil) {
self.urlString = urlString
self.networkOperationCompletionHandler = networkOperationCompletionHandler
super.init()
}
// when the operation actually starts, this is the method that will be called
override func main() {
request = Alamofire.request(urlString, method: .get, parameters: ["foo" : "bar"])
.responseJSON { response in
// do whatever you want here; personally, I'll just all the completion handler that was passed to me in `init`
self.networkOperationCompletionHandler?(response.result.value, response.result.error)
self.networkOperationCompletionHandler = nil
// now that I'm done, complete this operation
self.completeOperation()
}
}
// we'll also support canceling the request, in case we need it
override func cancel() {
request?.cancel()
super.cancel()
}
}
Then, when I want to initiate my 50 requests, I'd do something like this:
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
for i in 0 ..< 50 {
let operation = NetworkOperation(urlString: "http://example.com/request.php?value=\(i)") { responseObject, error in
guard let responseObject = responseObject else {
// handle error here
print("failed: \(error?.localizedDescription ?? "Unknown error")")
return
}
// update UI to reflect the `responseObject` finished successfully
print("responseObject=\(responseObject)")
}
queue.addOperation(operation)
}
That way, those requests will be constrained by the maxConcurrentOperationCount
, and we don't have to worry about any of the requests timing out..
This is an example AsynchronousOperation
base class, which takes care of the KVN associated with asynchronous/concurrent NSOperation
subclass:
//
// AsynchronousOperation.swift
//
// Created by Robert Ryan on 9/20/14.
// Copyright (c) 2014 Robert Ryan. All rights reserved.
//
import Foundation
/// Asynchronous Operation base class
///
/// This class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
/// a concurrent NSOperation subclass, you instead subclass this class which:
///
/// - must override `main()` with the tasks that initiate the asynchronous task;
///
/// - must call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
/// necessary and then ensuring that `completeOperation()` is called; or
/// override `cancel` method, calling `super.cancel()` and then cleaning-up
/// and ensuring `completeOperation()` is called.
public class AsynchronousOperation : Operation {
private let stateLock = NSLock()
private var _executing: Bool = false
override private(set) public var isExecuting: Bool {
get {
return stateLock.withCriticalScope { _executing }
}
set {
willChangeValue(forKey: "isExecuting")
stateLock.withCriticalScope { _executing = newValue }
didChangeValue(forKey: "isExecuting")
}
}
private var _finished: Bool = false
override private(set) public var isFinished: Bool {
get {
return stateLock.withCriticalScope { _finished }
}
set {
willChangeValue(forKey: "isFinished")
stateLock.withCriticalScope { _finished = newValue }
didChangeValue(forKey: "isFinished")
}
}
/// Complete the operation
///
/// This will result in the appropriate KVN of isFinished and isExecuting
public func completeOperation() {
if isExecuting {
isExecuting = false
}
if !isFinished {
isFinished = true
}
}
override public func start() {
if isCancelled {
isFinished = true
return
}
isExecuting = true
main()
}
override public func main() {
fatalError("subclasses must override `main`")
}
}
/*
Abstract:
An extension to `NSLocking` to simplify executing critical code.
Adapted from Advanced NSOperations sample code in WWDC 2015 https://developer.apple.com/videos/play/wwdc2015/226/
Adapted from https://developer.apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.zip
*/
import Foundation
extension NSLocking {
/// Perform closure within lock.
///
/// An extension to `NSLocking` to simplify executing critical code.
///
/// - parameter block: The closure to be performed.
func withCriticalScope<T>(block: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try block()
}
}
There are other possible variations of this pattern, but just ensure that you (a) return true
for asynchronous
; and (b) you post the necessary isFinished
and isExecuting
KVN as outlined the Configuring Operations for Concurrent Execution section of the Concurrency Programming Guide: Operation Queues.
Alamofire and Concurrent Operation Queues
The request delegate's dispatch queue is serial, meaning that it will only process a single block at a time, in order of when the blocks were dispatched (FIFO). If the second log statement isn't firing, it's because the previous block didn't yet finish.
How to cancel all requests in Alamofire's shared manager
Use NSURLSession
's invalidateAndCancel
method:
manager.session.invalidateAndCancel
Related Topics
Swift 3 Incorrect String Interpolation With Implicitly Unwrapped Optionals
Alamofire Get API Request Not Working as Expected
Swiftui Ios14 - Navigationview + List - Won't Fill Space
Why Are Emoji Characters Like 👩👩👧👦 Treated So Strangely in Swift Strings
Using a Type Variable in a Generic
Extend Array Types Using Where Clause in Swift
Dispatch_Once After the Swift 3 Gcd API Changes
Swift Variable Name With ' (Backtick)
Separating Multiple If Conditions With Commas in Swift
How to Convert Data to Hex String in Swift
What Is the Purpose of Willset and Didset in Swift
Swift: Print() VS Println() VS Nslog()
What Is a Good Example to Differentiate Between Fileprivate and Private in Swift3
Instance Member Cannot Be Used on Type
Transparent Background For Modally Presented Viewcontroller
Calling Protocol Default Implementation from Regular Method
Xcode 8 Beta 3: Expected ',' Joining Parts of a Multi-Clause Condition