> ## Content Index
> Fetch the complete content index at: https://williamboles.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Networking with NSOperation as Your Wingman
- URL: https://williamboles.com/networking-with-nsoperation-as-your-wingman/
- Published: 2016-02-04T15:28:13.000Z
- Updated: 2026-09-26T21:10:02.000Z
- Description: No one wants networking code scattered everywhere. Here, we look at how to utilise NSOperation to encapsulate each networking request within its own cancellable unit of work.
- Author: William Boles
- Tags: Networking, Concurrency, Stack Overflow API, NSOperation

Sometimes the tools that make our lives easier also make our architecture worse. When something is hard to do, we build structure around it, and that structure often leads us to make smarter decisions and be more thoughtful with what does what. When a new tool removes that difficulty, we remove the structure with it - and in doing so, lose the benefits we never even realised the structure gave us.

When [NSURLConnection](https://developer.apple.com/documentation/foundation/nsurlconnection?language=objc&ref=williamboles.com) was the only networking option on iOS, its [delegate](https://developer.apple.com/documentation/foundation/nsurlconnectiondelegate?language=objc&ref=williamboles.com)\-based approach required so much boilerplate that developers naturally pushed networking code out of their view controllers and into dedicated classes. The boilerplate was tedious, but it had an architectural side effect worth preserving - a distinct networking layer with clear responsibilities. When [NSURLSession](https://developer.apple.com/documentation/foundation/urlsession?language=objc&ref=williamboles.com) was released with its far more developer-friendly, closure-based interface, we jumped at the chance to remove all that boilerplate. But the ease of making network calls meant that we no longer had to keep networking at arm's length. Network requests, JSON parsing, and response handling started appearing directly in view controllers alongside their existing responsibilities 💥 - a quiet erosion of the [single responsibility principle](https://en.wikipedia.org/wiki/Single%5Fresponsibility%5Fprinciple?ref=williamboles.com) that made our codebases harder to understand and more brittle to change.

![Photo of a vintage aircraft](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2019/11/aircraft-vintage-min-1.jpg)

In this post, I want to explore how we can use [NSOperation](https://developer.apple.com/documentation/Foundation/Operation?language=objc&ref=williamboles.com) and [NSOperationQueue](https://developer.apple.com/documentation/foundation/operationqueue?language=objc&ref=williamboles.com) to get back to an isolated networking layer.

> This post is based on an `Objective-C` example. If you are more interested in seeing how to combine networking and operations based on a `Swift` example, head over to the more recent ["Building a networking layer with operations"](https://williamboles.com/building-a-networking-layer-with-operations/) post.

## Getting the Lay of the Land

Before jumping into the details, let's take a moment to look at the overall architecture we're going to put in place and how each component fits into it.

Our networking layer will have three main responsibilities:

1. Scheduling network requests.
2. Performing networking requests.
3. Parsing the response from network requests.

These responsibilities will come together to produce the following class structure:

![Class diagram showing the networking layer architecture. An external caller uses APIManager. APIManager creates an Operation and schedules it on OperationQueueManager. OperationQueueManager manages any Operation it has been given](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/2026/02/wingman-networking-operations-class-diagram.jpg)

- The entry point represents whichever feature triggers the data fetch; it could be a view controller, service, etc.
- `APIManager` creates an operation and schedules it on the `OperationQueueManager`. Ideally, the feature should never care where the data that powers it comes from; the `APIManager` ensures that it doesn't.
- `OperationQueueManager` is a singleton that controls when operations are executed.
- `Operation` is a concurrent `NSOperation` subclass that encapsulates the network request and parsing for a given endpoint.

> I've used generic naming in the class diagram, but in the example, we will be working in concrete types, so `APIManager` will become `NWMAnswersAPIManager` and `Operation` will become `NWMAnswersRetrievalOperation`, but any `APIManager` or `Operation` could be substituted in.

> Don't worry if that doesn't all make sense yet; we will look into each class in greater depth below.

Let's take a small recap of what `NSOperationQueue` and `NSOperation` are.

`NSOperationQueue` is responsible for coordinating the execution of operations. Rather than executing work immediately, it schedules operations based on each operation's readiness, priority, dependencies, and available system resources.

Because `NSOperationQueue` maintains visibility over its operations, we can inspect, pause, or cancel them — for example, cancelling in-flight requests when a user logs out.

Under the hood, `NSOperationQueue` leverages `GCD`, allowing it to take advantage of multiple cores without the developer needing to manage threads directly. By default, it will execute as many operations in parallel as the device can reasonably support.

`NSOperation` is an abstract class which needs to be subclassed to undertake a specific task. An `NSOperation` typically runs on a separate thread from the one that created it. Each operation is controlled via an internal state machine; the possible states are:

- `Pending` indicates that the operation has been added to the queue.
- `Ready` indicates that the operation is good to go, and if there is space on the queue, this operation's task can be started.
- `Executing` indicates that the operation is actually doing work at the moment.
- `Finished` indicates that the operation has completed its task and should be removed from the queue.
- `Cancelled` indicates that the operation has been cancelled and should stop its execution.

A typical operation's lifecycle will move through the following states:

![Operation state diagram showing an operations lifecycle going from Pending to Ready to Executing to Finished. It also shows how Pending, Ready and Executing can all end up in Cancelled](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2026/03/operation-lifecycle.jpg)

It's important to note that cancelling an executing operation will not automatically stop that operation; instead, it is up to the individual operation to clean up after itself and transition into the `Finished` state.

Operations come in two flavours:

- Non-Concurrent
- Concurrent

Non-Concurrent operations perform all their work on the same thread, so that when the `main` method returns, the operation is moved into the `Finished` state. The queue is then notified of this and removes the operation from its active operation pool, freeing resources for the next operation.

Concurrent operations can perform some of their work on a different thread, so returning from the `main` method can no longer be used to move the operation into a `Finished` state. Instead, when we create a concurrent operation, we assume the responsibility for moving the operation between the `Ready`, `Executing`, and `Finished` states.

> This post will gradually build up to a working example. But if you can't wait, then head on over to the [completed example](https://github.com/wibosco/NetworkingWingman-Example?ref=williamboles.com) to see how things end up.

## Building Your Wingman

A networking operation is a specialised concurrent operation because when an `NSURLSession` makes a network request, it does so on a different thread from the thread that *resumed* that task. Rather than cramming everything into one operation, this solution will focus on building an abstract concurrent operation and then subclassing it to do the fetching and parsing in the subclass.

As mentioned, a concurrent operation takes responsibility for ensuring that its internal state is correct. This state is controlled by manipulating the `isReady`, `isExecuting` and `isFinished` properties. However, these are **read-only**, so these properties will need to be overridden so that we can set them. Let's start with `isReady`:

```
@interface NWMOperation : NSOperation

@end

@implementation NWMOperation

 // 1
@synthesize ready = _ready;

#pragma mark - Init

// 2
- (instancetype)init
{
    self = [super init];

    if (self)
    {
        self.ready = YES;
    }

    return self;
}

#pragma mark - State

// 3
- (void)setReady:(BOOL)ready
{
    if (_ready != ready)
    {
        [self willChangeValueForKey:NSStringFromSelector(@selector(isReady))];
        _ready = ready;
        [self didChangeValueForKey:NSStringFromSelector(@selector(isReady))];
    }
}

// 4
- (BOOL)isReady
{
    return [super isReady] && _ready;
}

@end
```

Here's what we did above:

1. `NSOperation` declares `ready` as a read-only property. Overriding it with a custom setter and getter confuses the compiler's automatic ivar generation, so we need to explicitly synthesise the backing ivar `_ready` ourselves. Importantly, `_ready` is separate from the internal readiness state of `NSOperation` \- `@synthesize` creates a new piece of storage that lives only in this subclass.
2. `_ready` is set to `YES` at init, signalling that the operation has no internal reason to block execution. This is necessary because `BOOL` ivars default to `NO`, and nothing else in the class ever sets `_ready` back to `YES`. Without this, `isReady` would always return `NO` and the queue would never start the operation.
3. The setter wraps the state change in [KVO](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html?ref=williamboles.com) `willChangeValueForKey:` and `didChangeValueForKey:` calls. `NSOperationQueue` uses `KVO` to *know* when its operations change state, so without these notifications, the queue would never know the operation's readiness changed. The guard `_ready != ready` prevents redundant `KVO` notifications.
4. `[super isReady]` and `_ready` are completely independent pieces of state. `[super isReady]` is managed internally by `NSOperation` and tracks things like whether dependencies have finished - we never write to it. `_ready` is managed by this subclass. By combining them, the operation is only considered ready when both the internal conditions of `NSOperation` are satisfied *and* the operation hasn't already started.

We then repeat this pattern with `isExecuting` and `isFinished`:

```
@implementation NWMOperation

// Omitted other functionality

@synthesize executing = _executing;
@synthesize finished = _finished;

// Omitted other functionality

- (void)setExecuting:(BOOL)executing
{
    if (_executing != executing)
    {
        [self willChangeValueForKey:NSStringFromSelector(@selector(isExecuting))];
        _executing = executing;
        [self didChangeValueForKey:NSStringFromSelector(@selector(isExecuting))];
    }
}

- (BOOL)isExecuting
{
    return _executing;
}

- (void)setFinished:(BOOL)finished
{
    if (_finished != finished)
    {
        [self willChangeValueForKey:NSStringFromSelector(@selector(isFinished))];
        _finished = finished;
        [self didChangeValueForKey:NSStringFromSelector(@selector(isFinished))];
    }
}

- (BOOL)isFinished
{
    return _finished;
}

@end
```

> Note that we don't call `super` in `isExecuting` and `isFinished`.

Now that we have control of our operation's state, let's use that control by transiting from `Ready` \-> `Executing` by overriding the `start` method:

```
@implementation NWMOperation

// Omitted other functionality

// 1
- (void)start
{
    // 2
    if (self.isCancelled)
    {
        // TODO: Finish this operation
        return;
    }

    // 3
    if (!self.isExecuting)
    {
        // 4
        self.ready = NO;
        self.executing = YES;
        self.finished = NO;
    }
}

@end
```

Here's what we did:

1. `start` is called by the queue when the operation is ready for execution. By overriding `start` rather than `main`, this operation takes full control of its lifecycle - the queue will not automatically move it into the `Finished` state when the method returns. This is essential for concurrent operations where the real work (e.g. a network request) happens on a different thread and completes later.
2. An operation can start in the cancelled state, so we check `self.isCancelled`, and if this operation has been cancelled, we finish this operation (will see that next).
3. The `!self.isExecuting` check prevents the operation from being started twice. Without this, a second call to `start` would re-trigger `KVO` notifications and potentially confuse the queue.
4. Update the `isReady`, `isExecuting` and `isFinished` states to ensure that the operation correctly represents its started nature.

> Note that `[super start]` is intentionally not called. The default `start` implementation would call `main` and manage the state internally, which would conflict with the manual state management happening here. Subclasses of `NWMOperation` override `start`, and will call `[super start]` (which calls *this* implementation, not the implementation of `NSOperation`).

Let's fill in that `TODO`:

```
@interface NWMOperation : NSOperation

// 1
- (void)finish;

@end

@implementation NWMOperation

// Omitted other functionality

- (void)finish
{
    // 2
    if (self.executing)
    {
        // 3
        self.executing = NO;
        self.finished = YES;
    }
}

@end
```

Here's what we did:

1. `finish` acts as a nice symmetrical opposite of the `start` method, ensuring that when an operation has finished, it is moved into the `Finished` state. It's essential that all operations eventually call this method. As it's to be called by subclasses of `NWMOperation`, we expose it in the header.
2. The `self.isExecuting` check prevents the operation from being finished twice. Without this, a second call to `finish` would re-trigger `KVO` notifications and potentially confuse the queue.
3. Update the `isExecuting` and `isFinished` states to ensure that the operation correctly represents its stated nature. There is no need to update `isReady` as this is already set to `NO`.

> If you are experiencing odd behaviour where your queue seems to have jammed, and no operations are being processed, one of your operations is probably missing a finish call somewhere.

As operations can be cancelled, let's override `cancel` to call `finish`:

```
@implementation NWMOperation

// Omitted other functionality

- (void)cancel
{
    [super cancel];

    [self finish];
}

@end
```

It's important that regardless of how a concurrent operation ends, it moves into the `Finished` state.

All that's left to do is to override the `isAsynchronous`:

```
@implementation NWMOperation

- (BOOL)isAsynchronous
{
    return YES;
}

@end
```

As `NWMOperation` subclasses are always expected to be executed via an `NSOperationQueue`, overriding `isAsynchronous` is strictly not needed, but we override it here to express intent clearly.

> `isAsynchronous` only matters if we call `start` directly on the operation without a queue. In that case, the caller is supposed to check `isAsynchronous` to decide whether to spin up a separate thread.

Armed with an asynchronous parent class, let's add subclasses that can make network requests to take advantage of this new functionality.

## Giving Your Wingman a Target

Now that we've built the concurrent operation machinery piece by piece, we can see how little a concrete operation actually needs to add.

StackOverflow has an excellent, open [API](https://api.stackexchange.com/docs?ref=williamboles.com) that we're going to use to build a networking operation.

This networking operation subclass will retrieve and parse the latest answers via the `/answers` endpoint.

> The JSON structure of the `/answers` response and that response's subsequent parsing aren't shown below as they are not the central theme of this post (which is about the architecture of making a network request), but if you are interested in seeing everything working together, check out the [example project](https://github.com/wibosco/NetworkingWingman-Example?ref=williamboles.com).

Let's look at the complete operation and then examine 👀 the interesting parts:

```
// 1
@interface NWMAnswersRetrievalOperation : NWMOperation

@end

// 2
@interface NWMAnswersRetrievalOperation ()

@property (nonatomic, copy) void (^completion)(NSArray *answers);

@end

@implementation NWMAnswersRetrievalOperation

#pragma mark - Init

// 3
- (instancetype)initWithCompletion:(void (^)(NSArray *answers))completion
{
    self = [super init];

    if (self)
    {
        self.completion = completion;
        self.name = @"Answers-Retrieval";
    }

    return self;
}

#pragma mark - Start

// 4
- (void)start
{
    [super start];

    NSURLSession *session = [NSURLSession sharedSession];

    NSURL *url = [NSURL URLWithString:@"https://api.stackexchange.com/2.2/answers?site=stackoverflow"];

    NSURLSessionDataTask *task = [session dataTaskWithURL:url
                                        completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
                                  {
                                      NSDictionary *answersJSON = [NSJSONSerialization JSONObjectWithData:data
                                                                                                  options:NSJSONReadingMutableContainers
                                                                                                    error:nil];

                                      NWMAnswerParser *parser = [[NWMAnswerParser alloc] init];
                                      NSArray *answers = [parser parseAnswers:answersJSON[@"items"]];

                                      if (self.completion)
                                      {
                                          self.completion(answers);
                                      }

                                      [self finish];
                                  }];

    [task resume];
}

@end
```

Here's what we did above:

1. `NWMAnswersRetrievalOperation` subclasses `NWMOperation`, inheriting all the concurrent operation machinery - the manual state management, KVO notifications, and `start`/`finish` lifecycle. This means it only needs to focus on the specifics of its own network request and parsing.
2. The class extension declares a private `completion` block property. This property is only set once during init and called once from the network response callback.
3. Store the completion block and give the operation a human-readable name - the name is useful for debugging.
4. `start` is where the actual work happens. `[super start]` is called first, which triggers `NWMOperation`'s state transition from `Ready` to `Executing`. Then an `NSURLSession` data task is created and resumed. When the task completes on a background thread, the response is parsed into model objects via `NWMAnswerParser`, the completion block is called with the results, and `[self finish]` transitions the operation into the `Finished` state so the queue can reclaim the concurrency slot. The order matters - `finish` must be called *after* the completion block, otherwise the operation could be deallocated before the block executes.

Now that we have an operation making a network request, we need to schedule that operation.

## The Support Crew

So far, **2** of the **3** responsibilities of the networking layer shown above have been built:

- Performing networking requests
- Parsing the response from network requests

Time to look at the final responsibility:

- Scheduling network requests.

If we refer back to the class structure above, this responsibility is handled by both the `APIManager` and `OperationQueueManager` classes. The `APIManager` knows about a specific operation, and the `OperationQueueManager` knows how to schedule an operation on its queue. Let's start with the `OperationQueueManager`:

```
@interface NWMOperationQueueManager : NSObject

// 1
+ (instancetype)sharedInstance;

// 2
- (void)addOperation:(NSOperation *)operation;
```

```
@interface NWMOperationQueueManager ()

// 3
@property (nonatomic, strong) NSOperationQueue *queue;

@end

@implementation NWMOperationQueueManager

#pragma mark - SharedInstance

+ (instancetype)sharedInstance
{
    static NWMOperationQueueManager *sharedInstance = nil;

    // 4
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^
                  {
                      sharedInstance = [[NWMOperationQueueManager alloc] init];
                  });

    return sharedInstance;
}

#pragma mark - Init

- (instancetype)init
{
    self = [super init];

    if (self)
    {
        self.queue = [[NSOperationQueue alloc] init];
    }

    return self;
}

#pragma mark - AddOperation

- (void)addOperation:(NSOperation *)operation
{
    [self.queue addOperation:operation];
}

@end
```

Here's what we did:

1. `NWMOperationQueueManager` is a singleton. Using a singleton means all operations across the app are funnelled through the same manager, giving us a single point of control.
2. `addOperation:` is the only way to schedule work through this manager. It accepts any `NSOperation` subclass, which keeps the interface generic - the manager doesn't need to know about `NWMAnswersRetrievalOperation` or any other concrete operation type.
3. The class extension declares a private `NSOperationQueue`. By hiding the queue behind `addOperation:`, external code can never directly manipulate it - they can't pause it, change its concurrency limits, or remove operations. This encapsulation also makes it straightforward to expand later, for example, adding a second queue for system events versus user-driven events, with each `APIManager` specifying which queue to target.
4. The `dispatch_once` block ensures the shared instance is only created once, even if multiple threads call `sharedInstance` simultaneously.

> `NWMOperationQueueManager` adds every operation it's given, even if an identical request is already on the queue. To see how the queue manager can spot and merge those duplicates, check out [Coalescing Operations to Avoid Duplicated Work](https://williamboles.com/coalescing-operations-to-avoid-duplicated-work/).

With an operation queue to schedule work on, let's build the `APIManager`:

```
@interface NWMAnswersAPIManager : NSObject

// 1
+ (void)retrieveAnswersWithCompletion:(void (^)(NSArray *answers))completion;

@end
```

```
@implementation NWMAnswersAPIManager

#pragma mark - Retrieval

+ (void)retrieveAnswersWithCompletion:(void (^)(NSArray *answers))completion
{
    // 2
    NWMAnswersRetrievalOperation *operation = [[NWMAnswersRetrievalOperation alloc] initWithCompletion:completion];

    [[NWMOperationQueueManager sharedInstance] addOperation:operation];
}

@end
```

Here's what we did above:

1. `retrieveAnswersWithCompletion:` is a class method (`+`), so callers don't need to create an instance of the manager. Using a class method keeps view controller code clean - no manager instances to store as properties or manage lifecycle for. The completion block follows the same pattern as the operation, returning an `NSArray` of parsed answers or `nil`.
2. Creates a configured `NWMAnswersRetrievalOperation` with the caller's completion block, and schedules it on the shared `NWMOperationQueueManager`. The caller never needs to know that operations or queues exist - they ask for answers and get a callback. The boundary between feature and fetching isolates both parts and helps to ensure clear responsibilities.

And with that, all three responsibilities are now in place! 🪇

## Going Home

In some ways, we got lucky that `NSURLConnection` was hard to use. That difficulty forced us to build structure around our networking code, and that structure led us to make smarter decisions about where functionality lived.

When `NSURLSession` removed the difficulty, the structure went with it.

Building a networking layer around `NSOperation` isn't about making things harder again. It's about restoring the boundaries that keep responsibilities clear.

To see the complete working example, visit the [repository](https://github.com/wibosco/NetworkingWingman-Example?ref=williamboles.com) and clone the project.