NSOperation, nowadays known as simply ‘Operation’ since Swift removed all the NS prefixes (party pooper!), is a handy way of scheduling tasks to run in specific order, with explicit dependencies, with a certain number running in parallel. If you stretch the metaphor a little, you might say it’s Apple’s closest thing to async/await - however nowadays there’s also GCD and Combine which are also similar-ish to that.
Anyway, [NS]Operation and [NS]OperationQueue sometimes come in handy, especially when writing command-line tools in Swift. In this article I’ll explain how to get them to work in Asynchronous mode, using isAsynchronous
(which is a replacement for the old isConcurrent
property). There’s a lot of mucking around with KVO, which is like juggling chainsaws. Anyway, here is the helper class:
class AsyncOperation: Operation {
override func start() {
// Apple's docs say not to call super here.
guard !isCancelled else {
asyncFinish()
return
}
setIsExecutingWithKVO(value: true)
asyncStart()
}
/// Override this (no need to call super) to start your code.
func asyncStart() {}
/// Call this when you're done.
func asyncFinish() {
setIsExecutingWithKVO(value: false)
setIsFinishedWithKVO(value: true)
}
override var isAsynchronous: Bool {
return true
}
// MARK: KVO helpers.
// Cannot simply override the existing named fields because
// they are get-only and we need KVO.
private var myFinished = false
private var myExecuting = false
override var isFinished: Bool {
return myFinished
}
override var isExecuting: Bool {
return myExecuting
}
func setIsFinishedWithKVO(value: Bool) {
willChangeValue(forKey: "isFinished")
myFinished = value
didChangeValue(forKey: "isFinished")
}
func setIsExecutingWithKVO(value: Bool) {
willChangeValue(forKey: "isExecuting")
myExecuting = value
didChangeValue(forKey: "isExecuting")
}
}
To use it, subclass and simply override asyncStart
, do your thing, and call asyncFinish
when done. Like so:
class FooOperation: AsyncOperation {
// !! Override this, don't call super.
override func asyncStart() {
// Do eg some network call that returns later...
let url = URL(string: "http://example.com")!
URLSession.shared.dataTask(with: url, completionHandler: {
[weak self] data, response, error in
// !! Call this when you're done.
self?.asyncFinish()
}).resume()
}
}
And for bonus points, you can wrap this up into JS-alike promises with this helper class:
class PromiseOperation: AsyncOperation {
typealias PromiseClosure = (@escaping () -> ()) -> ()
let closure: PromiseClosure
init(closure: @escaping PromiseClosure) {
self.closure = closure
super.init()
}
override func asyncStart() {
closure { [weak self] in
self?.asyncFinish()
}
}
}
To use the PromiseOperation, no subclassing is needed. Simply instantiate like so, calling the ‘completion’ closure when done.
let fooPromise = PromiseOperation { completion in
// Do eg some network call that returns later...
let url = URL(string: "http://example.com")!
URLSession.shared.dataTask(with: url, completionHandler: {
[weak self] data, response, error in
// !! Call this when you're done.
completion()
}).resume()
}
myOperationQueue.addOperation(fooPromise)
Thanks for reading, I hope this helps someone, and have a great week!
Photo by Sandeep Damre on Unsplash
Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.
(Comp Sci, Hons - UTS)
Software Developer (Freelancer / Contractor) in Australia.
I have worked at places such as Google, Cochlear, Assembly Payments, News Corp, Fox Sports, NineMSN, FetchTV, Coles, Woolworths, Trust Bank, and Westpac, among others. If you're looking for help developing an iOS app, drop me a line!
Get in touch:
[email protected]
github.com/chrishulbert
linkedin