AsyncOperation

zeitschlag

Nathan Mattes

Posted on March 13, 2020

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:

  1. Download this.
  2. Download that.
  3. Download those items over there, too.
  4. Do something different, when you're done.
  5. 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!

💖 💪 🙅 🚩
zeitschlag
Nathan Mattes

Posted on March 13, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related