Search code examples
iosobjective-casynchronousnsoperationqueuensblockoperation

iOS throttling async API calls using NSBlockOperation


I want to limit the number of inflight API calls to 2. I can create an NSOperationQueue and add the block to the queue, however each API call has a completion block, so the initial calls are limited but I don't know how to limit the processing of the queue based on the execution of the completion block.

In the code below it is possible to have more than 2 calls APIs in flight at anyone time.

NSOperationQueue *requestQueue = [[NSOperationQueue alloc] init];
service.requestQueue.maxConcurrentOperationCount = 2;

for (int i = 0; i < 100; i++)
{
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{

        [self invokeAPI:kAPIName completion:^BOOL(APIResult *result) {

            // Do stuff
        }

         [requestQueue addOperation:operation];
    }
}

Would appreciate any pointers to the correct pattern to use.

EDIT - Based on Marc-Alexandre's answer

Have created this class to encapsulate the operations, is the approach safe from a memory point of view, given that this class will be created from the dataAccessService and injected, as well as the completion block having references to self, and the finish called before the completion block executes?

@interface MAGApiOperation : NSOperation

@property (nonatomic, strong) id<MAGDataAccessServiceProtocol> dataAccessService;
@property (nonatomic, copy) NSString *apiName;
@property (nonatomic, copy) BOOL (^onCompletion)(APIResult *);

+ (instancetype)apiOperationWithName:(NSString *)apiName dataAccessService:(id<MAGDataAccessServiceProtocol>)dataAccessService completion:(BOOL (^)(APIResult *))onCompletion;

@implementation MAGApiOperation

@synthesize executing = _isExecuting;
@synthesize finished = _isFinished;

#pragma mark - Class methods

/// Creates a new instance of MAGApiOperation
+ (instancetype)apiOperationWithName:(NSString *)apiName dataAccessService:(id<MAGDataAccessServiceProtocol>)dataAccessService completion:(BOOL (^)(APIResult *))onCompletion {

    MAGApiOperation *operation = [[self alloc] init];
    operation.apiName = apiName;
    operation.dataAccessService = dataAccessService;
    operation.onCompletion = onCompletion;

    return operation;
}

#pragma mark - NSOperation method overrides

- (void)start {

    [self willChangeValueForKey:@"isExecuting"];
    _isExecuting = YES;
    [self didChangeValueForKey:@"isExecuting"];

    if (!self.isCancelled)
    {
        [self invokeApiWithName:self.apiName completion:self.onCompletion];
    }
}

- (void)finish {

    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];

    _isExecuting = NO;
    _isFinished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

#pragma mark - Private methods

/// Invokes the api with the name then executes the completion block
- (void)invokeApiWithName:(NSString *)apiName completion:(BOOL (^)(VAAInvokeAPIResult *))onCompletion {

        [self.dataAccessService invokeAPI:kAPIName completion:^BOOL(APIResult *result) { {

            [self finish];

            return onCompletion(result);
        }];
}

Solution

  • You need to subclass NSOperation in order to do that.

    Here's the full documentation that explains it all how to subclass NSOperation: https://developer.apple.com/reference/foundation/operation

    Quick notes:

    • Your "start" operation would be the invokeAPI call.
    • Then in your invokeAPI completion block, you mark your own operation as finished (see below, note the willChange and didChange calls are really important)

    [self willChangeValueForKey:@"isExecuting"];
    [self willChangeValueForKey:@"isFinished"];
    self.isExecuting = NO;
    self.isFinished = YES;
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];