You asked:
How do I make one operation not start until the previous operation finishes?
To do this, you could, theoretically, simply make a serial queue (which is fine if you want to make all operations wait until the prior one finishes). With an NSOperationQueue
, you achieve that simply by setting maxConcurrentOperationCount
to 1
.
Or, a little more flexible, you could establish dependencies between operations where dependencies are needed, but otherwise enjoy concurrency. For example, if you wanted to make two network requests dependent upon the completion of a third, you could do something like:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4; // generally with network requests, you don't want to exceed 4 or 5 concurrent operations;
// it doesn't matter too much here, since there are only 3 operations, but don't
// try to run more than 4 or 5 network requests at the same time
NSOperation *operation1 = [[NetworkOperation alloc] initWithRequest:request1 completionHandler:^(NSData *data, NSError *error) {
[self doSomethingWithData:data fromRequest:request1 error:error];
}];
NSOperation *operation2 = [[NetworkOperation alloc] initWithRequest:request2 completionHandler:^(NSData *data, NSError *error) {
[self doSomethingWithData:data fromRequest:request2 error:error];
}];
NSOperation *operation3 = [[NetworkOperation alloc] initWithRequest:request3 completionHandler:^(NSData *data, NSError *error) {
[self doSomethingWithData:data fromRequest:request3 error:error];
}];
[operation2 addDependency:operation1]; // don't start operation2 or 3 until operation1 is done
[operation3 addDependency:operation1];
[queue addOperation:operation1]; // now add all three to the queue
[queue addOperation:operation2];
[queue addOperation:operation3];
You asked:
How do I ensure that an operation will not complete until the asynchronous network request it issued has finished as well?
Again, there are different approaches here. Sometimes you can avail yourself with semaphores to make asynchronous process synchronous. But, much better is to use a concurrent NSOperation
subclass.
An "asynchronous" NSOperation
is simply one that will not complete until it issues a isFinished
notification (thereby allowing any asynchronous tasks it initiates to finish). And an NSOperation
class specifies itself as an asynchronous operation simply by returning YES
in its isAsynchronous
implementation. Thus, an abstract class implementation of an asynchronous operation might look like:
// AsynchronousOperation.h
@import Foundation;
@interface AsynchronousOperation : NSOperation
/**
Complete the asynchronous operation.
If you create an asynchronous operation, you _must_ call this for all paths of execution
or else the operation will not terminate (and dependent operations and/or available
concurrent threads for the operation queue (`maxConcurrentOperationCount`) will be blocked.
*/
- (void)completeOperation;
@end
and
//
// AsynchronousOperation.m
//
#import "AsynchronousOperation.h"
@interface AsynchronousOperation ()
@property (getter = isFinished, readwrite) BOOL finished;
@property (getter = isExecuting, readwrite) BOOL executing;
@end
@implementation AsynchronousOperation
@synthesize finished = _finished;
@synthesize executing = _executing;
- (instancetype)init {
self = [super init];
if (self) {
_finished = NO;
_executing = NO;
}
return self;
}
- (void)start {
if (self.isCancelled) {
if (!self.isFinished) self.finished = YES;
return;
}
self.executing = YES;
[self main];
}
- (void)completeOperation {
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}
#pragma mark - NSOperation methods
- (BOOL)isAsynchronous {
return YES;
}
- (BOOL)isExecuting {
@synchronized(self) { return _executing; }
}
- (BOOL)isFinished {
@synchronized(self) { return _finished; }
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
@synchronized(self) { _executing = executing; }
[self didChangeValueForKey:@"isExecuting"];
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
@synchronized(self) { _finished = finished; }
[self didChangeValueForKey:@"isFinished"];
}
@end
Now that we have that abstract, asynchronous NSOperation
subclass, we can use it in our concrete NetworkOperation
class:
#import "AsynchronousOperation.h"
NS_ASSUME_NONNULL_BEGIN
typedef void(^NetworkOperationCompletionBlock)(NSData * _Nullable data, NSError * _Nullable error);
@interface NetworkOperation : AsynchronousOperation
@property (nullable, nonatomic, copy) NetworkOperationCompletionBlock networkOperationCompletionBlock;
@property (nonatomic, copy) NSURLRequest *request;
- (instancetype)initWithRequest:(NSURLRequest *)request completionHandler:(NetworkOperationCompletionBlock)completionHandler;
@end
NS_ASSUME_NONNULL_END
and
// NetworkOperation.m
#import "NetworkOperation.h"
@interface NetworkOperation ()
@property (nonatomic, weak) NSURLSessionTask *task;
@end
@implementation NetworkOperation
- (instancetype)initWithRequest:(NSURLRequest *)request completionHandler:(NetworkOperationCompletionBlock)completionHandler {
self = [self init];
if (self) {
self.request = request;
self.networkOperationCompletionBlock = completionHandler;
}
return self;
}
- (void)main {
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionTask *task = [session dataTaskWithRequest:self.request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (self.networkOperationCompletionBlock) {
self.networkOperationCompletionBlock(data, error);
self.networkOperationCompletionBlock = nil;
}
[self completeOperation];
}];
[task resume];
self.task = task;
}
- (void)cancel {
[super cancel];
[self.task cancel];
}
@end
Now, in this example, I'm using block-based implementation of these asynchronous network requests, but the idea works equally well in delegate-based connections/sessions, too. (The only hassle is that NSURLSession
specifies its task-related delegate methods to be part of the session, not the network task.)
Clearly the implementation of your own NetworkOperation
class may differ wildly (use delegate patterns or completion block patterns, etc.), but hopefully this illustrates the idea of a concurrent operation. For more information, see the Operation Queues chapter of the Concurrency Programming Guide, notably the section titled "Configuring Operations for Concurrent Execution".