Instead of semaphores or groups that others have advised (which blocks a thread, which can be problematic if you have too many threads blocked), I would use a custom, asynchronous NSOperation
subclass for network requests. Once you've wrapped the request in an asynchronous NSOperation
, you can then add a bunch of operations to an operation queue, not blocking any threads, but enjoying dependencies between these asynchronous operations.
For example, a network operation might look like:
class NetworkOperation: AsynchronousOperation {
private let url: NSURL
private var requestCompletionHandler: ((NSData?, NSURLResponse?, NSError?) -> ())?
private var task: NSURLSessionTask?
init(url: NSURL, requestCompletionHandler: (NSData?, NSURLResponse?, NSError?) -> ()) {
self.url = url
self.requestCompletionHandler = requestCompletionHandler
super.init()
}
override func main() {
task = NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
self.requestCompletionHandler?(data, response, error)
self.requestCompletionHandler = nil
self.completeOperation()
}
task?.resume()
}
override func cancel() {
requestCompletionHandler = nil
super.cancel()
task?.cancel()
}
}
/// 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 : NSOperation {
override public var asynchronous: Bool { return true }
private let stateLock = NSLock()
private var _executing: Bool = false
override private(set) public var executing: Bool {
get {
return stateLock.withCriticalScope { _executing }
}
set {
willChangeValueForKey("isExecuting")
stateLock.withCriticalScope { _executing = newValue }
didChangeValueForKey("isExecuting")
}
}
private var _finished: Bool = false
override private(set) public var finished: Bool {
get {
return stateLock.withCriticalScope { _finished }
}
set {
willChangeValueForKey("isFinished")
stateLock.withCriticalScope { _finished = newValue }
didChangeValueForKey("isFinished")
}
}
/// Complete the operation
///
/// This will result in the appropriate KVN of isFinished and isExecuting
public func completeOperation() {
if executing {
executing = false
finished = true
}
}
override public func start() {
if cancelled {
finished = true
return
}
executing = true
main()
}
}
// this locking technique taken from "Advanced NSOperations", WWDC 2015
// https://developer.apple.com/videos/play/wwdc2015/226/
extension NSLock {
func withCriticalScope<T>(@noescape block: Void -> T) -> T {
lock()
let value = block()
unlock()
return value
}
}
Having done that, you can initiate a whole series of requests that could be performed sequentially:
let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 1
for urlString in urlStrings {
let url = NSURL(string: urlString)!
print("queuing \(url.lastPathComponent)")
let operation = NetworkOperation(url: url) { data, response, error in
// do something with the `data`
}
queue.addOperation(operation)
}
Or, if you don't want to suffer the significant performance penalty of sequential requests, but still want to constrain the degree of concurrency (to minimize system resources, avoid timeouts, etc), you can set maxConcurrentOperationCount
to a value like 3 or 4.
Or, you can use dependencies, for example to trigger some process when all of the asynchronous downloads are done:
let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 3
let completionOperation = NSBlockOperation() {
self.tableView.reloadData()
}
for urlString in urlStrings {
let url = NSURL(string: urlString)!
print("queuing \(url.lastPathComponent)")
let operation = NetworkOperation(url: url) { data, response, error in
// do something with the `data`
}
queue.addOperation(operation)
completionOperation.addDependency(operation)
}
// now that they're all queued, you can queue the completion operation on the main queue, which will only start once the requests are done
NSOperationQueue.mainQueue().addOperation(completionOperation)
And if you want to cancel the requests, you can easily cancel them:
queue.cancelAllOperations()
Operations are incredibly rich mechanism for controlling a series of asynchronous tasks. If you refer to WWDC 2015 video Advanced NSOperations, they have taken this pattern to a whole other level with conditions and observers (though their solution might be a bit overengineered for simple problems. IMHO).