Why this post?

There are plenty of resources on NSOperation out in there, so it is probably just more noise in the ether.

However, it is also one of the questions I get the most from industry colleagues, because there’s nuance and that makes it tough to commit to memory or concretely discover with a Google search.

So this post will not go into the benefits of NSOperation or give a history review of the API over the years or even provide examples of using the API… it is just going to document how to subclass NSOperation so that it can be used for your use cases.

Short NSOperation Preamble

Having used NSOperation since its introduction in Mac OS X 10.5, I have seen it go through a bumpy ride. It definitely has a history of bugs that have hurt the developer community’s perception for this API, but as of iOS 10 / macOS 10.12 it has stabilized and is very much the way to organize large units of work on Apple platforms. As interesting as going through the history of problems would be, that is in the past and we can focus on it being solid now. That’s means (at the time of this post) there are 5 releases worth of rock solid support, so it isn’t the same risk as it has been in the past.

Putting everything “substantial” in an NSOperation gives you many features to leverage that would be complicated to build on your own, not to mention inefficient compared to Apple’s own optimized API. I’m not going to go through the benefits or features in this post, just keeping it on topic to “how to subclass NSOperation”.

Note on async await

The new async await available for iOS 15+ introduced at WWDC 2021 is an excellent addition to Swift that will offer a great deal of value to developers doing async and concurrent programming. There is no competition between async await and NSOperation though; saying that NSOperation is no longer useful since async await was introduced would be akin to saying NSOperation is no longer useful because GCD exists. They serve different purposes and actually complement each other. You can implement your NSOperation (Operation in Swift) using async await without issue! Update: I actually spent a good amount of time on this and can’t figure out a way to implement NSOperation using async await… It is simple enough to use NSOperation from async contexts, but I could not get the KVO inside NSOperation to work using the new concurrency features of Swift 🤷‍♂️.

NSOperation Intro

NSOperation effectively offers an API to encapsulate a unit of work that can be submitted to a priority queue, an NSOperationQueue.

The NSOperation class itself is not terribly useful. To really have utility, it must be subclassed so that “work” can be executed and the pattern of NSOperation can be adopted to make use of all the benefits that NSOperation has to offer.

Apple gives use two concrete subclasses as a way to get started, which effectively enables 3 specific ways of performing work with an NSOperation.

  1. Block based work can use NSBlockOperation
  2. Objective-C object based work can use NSInvocationOperation
    • This makes it possible to have the work performed via target and selector, or via an NSInvocation

As valuable as having these convient concrete classes are, more control can be necessary for complex operations. This is where custom subclasses come in.

Anatomy of an NSOperation

The NSOperation has a number of properties that control how it works (obviously).

There are KVO properties for controlling the operation execution and signal its progress:

  • isCancelled
  • isExecuting
  • isFinished
  • isReady

There are properties controlling it’s exectuion:

  • qualityOfService
  • queuePriority
  • dependencies
  • completionBlock

The property to distinguish its operating behavior:

  • isAsynchronous

Asynchronous vs Synchronous Operations

Before we go into the other parts of NSOperation, it is worth calling out up front that there are two modes for NSOperation: isAsynchronous being NO and isAsynchronous being YES.

This property disambiguates the way the operation operations (asynchronously vs synchronously), but actually completely splits how we subclass NSOperation. This is probably the most important callout to make when subclassing NSOperation – the single base class has two completely different patterns to follow when subclassing based on whether the operation is asynchronous or synchronous.

Effectively, synchronous NSOperation subclasses can defer all of the KVO related bookkeeping to the super class’ implementation. They just need to implement the “work”, to run synchronously, and that is it. All the Apple provided concrete NSOperation classes (NSBlockOperation and NSInvocationOperation) are synchronous.

When it comes to asynchronous subclassing, there is a great deal of care and nuance required to properly having things operate within the system.

The KVO Properties of NSOperation

The execution of an NSOperation (and other dependent NSOperation instances for that matter) is gated by the state of the KVO state properties.

First is isReady. While isReady is NO, the operation will not be dequeued by the NSOperationQueue. Once isReady becomes YES, the operation can start (as long as there is capacity for the NSOperationQueue to run it, of course).

Then, when the operation starts, it moves isExecuting from NO to YES.

Then the work happens, whether it is synchronous or asynchronous.

On the work’s completion, isExecuting goes back to NO.

Finally, the operation finishes by moving isFinished from NO to YES.

All of the above is completely managed for you by synchronous NSOperation subclasses. Asynchronous subclasses must manage this state and their transitions all on their own.

Additionally, there is the isCancelled property. By default, calling cancel on an NSOperation just flips isCancelled to YES. As work is executing, the “working” part of the execution is responsible for checking isCancelled at appropriate places and finishing early (returning early for synchronous operations, and setting isExecuting to NO and isFinished to YES for asynchronous operations).

isReady specifically

The isReady property deserves some special attention. The property prevents the operation from running in a queue until it is set to YES. This property is effectively coupled to all the dependencies of the NSOperation being isFinished == YES. Unfortunately, the implementation details are hidden so if you override isReady in your subclass you will not be able to properly reflect the dependency graph so overriding isReady effectively means you are replacing the behavior with your own and the dependencies will no longer have an effect.

Beyond isReady being coupled to dependencies being finished, it will also flip to YES when isCancelled is set to YES before the operation has actually started in its queue. This makes it possible to rapidly have the operation start and immediately finish, clearing it from the queue and unblocking operations that depend on it.

Ultimately, there is enough nuance and trouble around isReady that it is best to never override it. If you need to apply custom behavior to have isReady depend on, the simplest choice is to use other NSOperation objects as dependencies. Once you think of isReady as just areAllDependenciesFinished, it becomes much easier to reason about and you can easily build blocking behavior for running your operation with other operations as dependencies.

Implementing Synchronous NSOperation

Synchronous NSOperation subclassing is the most straightforward option, so let’s go through what that can look like.

Per the documentation, all you really need to do is implement main with synchronously executing work.

Objective-C Code

@interface MySyncOperation : NSOperation
@end

@implementation MySyncOperation

- (void)main
{
    ... run work ...

    if (self.isCancelled) {
       return;
    }
    
    ... run more work ...
    
    if (self.isCancelled) {
       return;
    }
    
    ... run final work ...
}

@end

Swift Code

class MySyncOperation: Operation {

    override func main() {
        ... run work ...
    
        guard !self.isCancelled else { return }
    
        ... run more work ...
    
        guard !self.isCancelled else { return }
    
        ... run final work ...
    }

}

Implementing Asynchronous NSOperation

Asynchronous NSOperation subclassing has a lot more nuance to it.

Instead of overriding main, you will override start. You also need to override the state properties.

It is important to keep things thread safe, you don’t know what thread any of the state accessors will be called from. In my sample implementations, I will use @synchronized(self) which effectively yields a recursive pthread mutex under the hood. If you want to use a different synchronization mechanism, feel free, but keep a few things in mind.

First, for maximum robustness, you will want to encapsulate multiple reads and writes to different states values within in a critical section. That means making each state value atomic is insufficient for maximum thread safety. You might be able to get away with only keeping your state values atomic, but unless you can prove there is a performance need, encapsulating state reads/writes with critical sections is going to be the simpler choice.

Second, you’ll also need to be sure that your critical sections support recursion. Since we are managing KVO as we update state, that will lead to accessing the KVO property while in the critical section (every willChangeValueForKey: and didChangeValueForKey: call will access the property of the given key). If you go with atomic state values instead of a critical section pattern, you won’t need to worry about recursion (but the race conditions will be more of a risk).

Objective-C Code

@interface MyAsyncOperation : NSOperation
@end

@implementation MyAsyncOperation
{
    struct {
        BOOL isCancelled;
        BOOL isExecuting;
        BOOL isFinished;
    } _state;
    dispatch_queue_t _queue;
}

- (instancetype)init
{
    if (self = [super init]) {
        _queue = ... the queue to execute on ...;
    }
    return self;
}

#pragma mark State Accessors

- (BOOL)isFinished
{
    @synchronized(self) {
        return _state.isFinished;
    }
}

- (BOOL)isExecuting
{
    @synchronized(self) {
        return _state.isExecuting;
    }
}

- (BOOL)isCancelled
{
    @synchronized(self) {
        return _state.isCancelled;
    }
}

- (BOOL)isAsynchronous
{
    return YES;
}

#pragma mark Method Overrides

- (void)start
{
    @synchronized(self) {
        if (_state.isCancelled) {
            [self _finish];
            return;
        }
        
        [self willChangeValueForKey:@"isExecuting"];
        _state.isExecuting = YES;
        [self didChangeValueForKey:@"isExecuting"];
    }
    
    dispatch_async(_queue, ^{
        [self _run];
    );
}

- (void)cancel
{
    @synchronized(self) {
        if (!_state.isCancelled) {
            [self willChangeValueForKey:@"isCancelled"];
            _state.isCancelled = YES;
            [self didChangeValueForKey:@"isCancelled"];
            
         [self _finish];
       }
    }
}

#pragma mark Private Methods

- (void)_run __attribute__((objc_direct))
{
    if (self.isCancelled) {
        return;
    }

    ... do work ...
    
    if (self.isCancelled) {
        return;
    }
    
    ... do more work ...
    
    if (self.isCancelled) {
        return;
    }
    
    @synchronized(self) {
        [self _finish];
    }
}

/* this method must be called from a safely synchronized critical section */
- (void)_finish __attribute__((objc_direct))
{
    const BOOL shouldFinish = !_state.isFinished;
    const BOOL shouldStopExecuting = _state.isExecuting;
    
    if (shouldFinish) {
        [self willChangeValueForKey:@"isFinished"];
    }
    if (shouldStopExecuting) {
        [self willChangeValueForKey:@"isExecuting"];
    }
    
    _state.isFinished = YES;
    _state.isExecuting = NO;
    
    if (shouldStopExecuting) {
        [self didChangeValueForKey:@"isExecuting"];
    }
    if (shouldFinish) {
        [self didChangeValueForKey:@"isFinished"];
    }
}

@end

Swift Code

Implemented with NSRecursiveLock. Same functionally as the Objective-C version. Can implement using async/await with Swift 5.5, but it will get somewhat messy with back and forth between async contexts and non-async contexts – feel free to share with me a clean implementation :) Update I could not figure out how to implement NSOperation in Swift using async await – if you can, please share!

class MyAsyncOperation: Operation {

    struct State {
        var isCancelled: Bool
        var isExecuting: Bool
        var isFinished: Bool
    }
    
    private let state = State()
    private let lock = NSRecursiveLock()
    private let queue: DispatchQueue
    
    init() {
        queue = ... the queue to execute on ...
    }

    // MARK: State Accessors

    public override var isFinished: Bool {
        self.lock.lock()
        defer { self.lock.unlock() }
        
        return self.state.isFinished
    }
    
    public override var isExecuting: Bool {
        self.lock.lock()
        defer { self.lock.unlock() }
        
        return self.state.isExecuting
    }
    
    public override var isCancelled: Bool {
        self.lock.lock()
        defer { self.lock.unlock() }
        
        return self.state.isCancelled
    }

    public override var isAsynchronous: Bool {
        return true
    }

    // MARK: Method Overrides

    public override func start() {
        self.lock.lock()
        defer { self.lock.unlock() }
        
        guard !self.state.isCancelled else {
            self.finish()
            return
        }
        
        self.willChangeValue(forKey: "isExecuting")
        self.state.isExecuting = true
        self.didChangeValue(forKey: "isExecuting")
        
        self.queue.async {
            self.run()
        }
    }
    
    public override func cancel() {
        self.lock.lock()
        defer { self.lock.unlock() }
        
        if !self.state.isCancelled {
            self.willChangeValue(forKey: "isCancelled")
            self.state.isCancelled = true
            self.didChangeValue(forKey: "isCancelled")
            self.finish()
        }
    }

    // MARK: Private Methods

    private func run() { 
        guard !self.isCancelled else {
            return
        }

        ... do work ...
        
        guard !self.isCancelled else {
            return
        }
        
        ... do more work ...
        
        guard !self.isCancelled else {
            return
        }
        
        self.lock.lock()
        defer { self.lock.unlock() }
        self.finish()
    }

    /* this func must be called from a safely synchronized critical section */
    private func finish() {
        let shouldFinish = !self.state.isFinished
        let shouldStopExecuting = self.state.isExecuting
        
        if shouldFinish {
            self.willChangeValue(forKey: "isFinished")
        }
        if shouldStopExecuting {
            self.willChangeValue(forKey: "isExecuting")
        }
        
        self.state.isFinished = true
        self.state.isExecuting = false
        
        if shouldStopExecuting {
            self.didChangeValue(forKey: "isExecuting")
        }
        if shouldFinish {
            self.didChangeValue(forKey: "isFinished")
        }
    }

}

Final Thoughts

NSOperation is a powerful way to encapsulate work and construct composition of work through dependencies and with the priority ordering of an NSOperationQueue. It is reliable, flexible and extendable to your use cases.

The biggest thing is to be sure you are properly implementing your NSOperation subclasses, which comes with a lot of nuance. Once you have it down once though, it is an easily repeatable process that can be leveraged at scale, especially if you abstract out the base implementation of an async operation.