AsyncOperation
A few days ago, I was working on an app for a client. With this app you can basically see a staff roster, that needs to be downloaded from a server. The REST-API is built that way, that the app has to talk to different endpoints to download all the data it needs. So the idea was basically:
- Download this.
- Download that.
- Download those items over there, too.
- Do something different, when you're done.
- There's not 5.
Sure, I could've added a 3rd-party-dependency that does all this and I'd be done. But personally, I try to use as little 3rd-party-dependencies as possible. And if I use one, I'd like to make sure, that they're not the most critical parts of the app. At the same time, I wanted to avoid putting a new URLSession.dataTask(with:completionHandler)
into another completionHandler
in the same file.
As downloading is a pretty important thing in an app, that downloads and displays information, I decided not to use Alamofire or other fancy frameworks, no Combine, no RxSwift, just Swift and some Foundation. For downloading things I used URLSession
and it URLSessionDataTasks
, as everyone would do today, I guess. For the orchestration of the tasks, I could've rebuilt Grand Central Dispatch as John Sundell describes in his blogpost Task-based concurreny in Swift. But actually, he summed up pretty well in half a sentence, how I was going to do it:
subclass Operation to create a very custom solution
First, I was thinking about using DispatchQueue
, DispatchGroup
and all their friends, but I didn't need all their capabilities. I wanted to use something more high-level, like Operation
and OperationQueue
. For this exact task, they gave me enough power to solve it. So, as John suggested, let's use Operation
and a pretty basic very custom solution.
All I needed is described in Apple's Concurrency Programming Guide. I created a subclass with the very generic name of AsyncOperation
with two properties asyncOperationFinished
and asyncOperationExecuting
and some KeyPath-observeration code, just like the guide suggested:
import Foundation
class AsyncOperation: Operation {
private var asyncOperationFinished: Bool
private var asyncOperationExecuting: Bool
override init() {
self.asyncOperationFinished = false
self.asyncOperationExecuting = false
super.init()
}
override var isFinished: Bool {
return self.asyncOperationFinished
}
override var isExecuting: Bool {
return self.asyncOperationExecuting
}
override func start() {
if (self.isCancelled) {
self.willChangeValue(for: \AsyncOperation.isFinished)
self.asyncOperationFinished = true
self.didChangeValue(for: \AsyncOperation.isFinished)
return
}
self.willChangeValue(for: \AsyncOperation.isExecuting)
self.asyncOperationExecuting = true
Thread.detachNewThreadSelector(#selector(main), toTarget: self, with: nil)
self.didChangeValue(for: \AsyncOperation.isExecuting)
}
override func cancel() {
super.cancel()
self.completeOperation()
}
func operationCompletedSuccessfully() {
self.completeOperation()
}
private func completeOperation() {
self.willChangeValue(for: \AsyncOperation.isFinished)
self.willChangeValue(for: \AsyncOperation.isExecuting)
self.asyncOperationExecuting = false
self.asyncOperationFinished = true
self.didChangeValue(for: \AsyncOperation.isFinished)
self.didChangeValue(for: \AsyncOperation.isExecuting)
}
}
For the actual asynchronous work like downloading and processing stuff, I subclassed this subclass again:
class DownloadFromThisEndpointOperation: AsyncOperation {
override func main() {
// download things from first endpoint asynchronously
// call self.cancel() in completion-block if somethings goes wrong
// call self.operationCompletedSuccessfully() in completion-block if you're done.
}
}
So I had five different, independent operations, each calling a different endpoint, processing the downloaded data and sending a notification to update the UI. But there's one more thing missing: I needed to know, when all the operations were done. To do this, I used another subclass of Operation
:
import Foundation
class DownloadFinishedOperation: Operation {
var downloadFinishedCompletion: ((Bool) -> ())?
init(downloadCompletion: ((Bool) -> ())?) {
self.downloadFinishedCompletion = downloadCompletion
}
override func main() {
let cancelledOperations = dependencies.filter { $0.isCancelled }
if cancelledOperations.count > 0 {
downloadFinishedCompletion?(false)
} else {
downloadFinishedCompletion?(true)
}
}
}
This synchrous operation gets a completion-block and if only one of the dependent operation failed earlier, the whole thing wasn't successful. Now, that I had all the pieces, the last step to do was to put them together. Thankfully, one can add dependencies to operations. So I connected every other operation to the last one, that should do something at the end, added all the operations to an instance of OperationQueue
and started the whole thing. That's it.
let allOperations = [
DownloadFromThisEndpointOperation()
DownloadFromThatEndpointOperation()
DownloadThoseItemsFromThatEndpointOperation()
]
let resultsOperation = DownloadFinishedOperation(downloadCompletion: completion)
for operation in allOperations {
resultsOperation.addDependency(operation)
}
var allOperationsIncludingResult = Array<Operation>(allOperations)
allOperationsIncludingResult.append(resultsOperation)
let operationQueue = OperationQueue()
operationQueue.addOperations(allOperationsIncludingResult, waitUntilFinished: false)
Conclusion
To be honest: I have no idea, if this is the way you do this. But for me, it worked flawlessly: I can download the data more or less asynchronously and indepedently from each other and get notified in the end. And more or less as a byproduct I got a class that I can use to chain asynchonous tasks together.
And it feels better than I thought. So you better use this very carefully!