How to create your own custom NSOperation subclass

Hi Guys,

Sometimes what happens is you are using a third party library to perform a particular task. Now you need to perform this task on an array in a sequential manner. Now comes the challenge how would you track the completion because it may end in some other thread.

At this point I feel a custom NSOperation really helps.

I got inspiration from AFNetworking custom operation “AFURLConnectionOperation.m”

First you need to implement

  1. start
  2. operationDidStart
  3. cancel

Then you need to maintain the states of the operation like

  1. Paused State
  2. Ready
  3. Executing
  4. Finished

So in my custom operation .h file I would create an enum like this

typedef NS_ENUM(NSInteger, ResourceDownloadOperationState)
{
 ResourceDownloadOperationPausedState      = -1,
 ResourceDownloadOperationReadyState       = 1,
 ResourceDownloadOperationExecutingState   = 2,
 ResourceDownloadOperationFinishedState    = 3,
};

After this I would create my own set of success , failure and progress block which I would need to pass in the custom init method of the NSOperation subclass say ResourceDownloadOperation.

typedef void(^NetworkManagerFailureBlock) (id errorMessage);
typedef void(^NetworkManagerProgressBlock) (id progressMessage, CGFloat progress);
typedef void(^NetworkManagerSuccessBlock) (id responseObject);

In the interface of my ResourceDownloadOperation class I would need the following properties

typedef NS_ENUM(NSInteger, ResourceDownloadOperationState)
{
    ResourceDownloadOperationPausedState      = -1,
    ResourceDownloadOperationReadyState       = 1,
    ResourceDownloadOperationExecutingState   = 2,
    ResourceDownloadOperationFinishedState    = 3,
};


#define DOC_DIRECTORY  [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]


@interface ResourceDownloadOperation : NSOperation

@property (nonatomic, copy) NetworkManagerSuccessBlock successBlock;
@property (nonatomic, copy) NetworkManagerFailureBlock failureBlock;
@property (nonatomic, copy) NetworkManagerProgressBlock progressBlock;

@property (nonatomic,strong) NSMutableURLRequest *request;
@property (nonatomic,strong) NSURLSessionDownloadTask *downloadTask;
@property (nonatomic,strong) AFURLSessionManager *urlManager;

- (instancetype)initWithRequest:(NSMutableURLRequest *)request successBlock:(NetworkManagerSuccessBlock)successBlock progressBlock:(NetworkManagerProgressBlock) progressBlock failureBlock : (NetworkManagerFailureBlock) failureBlock;

In the implementation file we will be declaring the private methods of the class like this

@interface ResourceDownloadOperation ()

@property (readwrite, nonatomic, assign) ResourceDownloadOperationState state;
@property (readwrite, nonatomic, strong) NSError *error;

- (void)operationDidStart;
- (void)finish;
- (void)cancelConnection;
@end

@implementation ResourceDownloadOperation

- (instancetype)initWithRequest:(NSMutableURLRequest *)request successBlock:(NetworkManagerSuccessBlock)successBlock progressBlock:(NetworkManagerProgressBlock) progressBlock failureBlock : (NetworkManagerFailureBlock) failureBlock
{
    if (self = [super init])
    {
        // 2: Set the properties.
        self.request  = request;
        self.successBlock  = successBlock;
        self.progressBlock = progressBlock;
        self.failureBlock  = failureBlock;

        AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
        securityPolicy.allowInvalidCertificates = YES;
        [securityPolicy setValidatesDomainName:NO];
        
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
        self.urlManager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
        self.urlManager.securityPolicy = securityPolicy;


        
    }
    _state = ResourceDownloadOperationReadyState;
    return self;
}

- (BOOL)isReady {
    return self.state == ResourceDownloadOperationReadyState && [super isReady];
}

- (BOOL)isExecuting {
    return self.state ==ResourceDownloadOperationExecutingState ;
}

- (BOOL)isFinished {
    return self.state == ResourceDownloadOperationFinishedState;
}

- (BOOL)isConcurrent {
    return NO;
}

//This method is called automatically
- (void)start
{
    if ([self isCancelled])
    {
        
        [self cancelConnection];
    }
    else if ([self isReady])
    {
        self.state = ResourceDownloadOperationExecutingState;
        [self operationDidStart];
        
    }
    
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    while(self.state != ResourceDownloadOperationFinishedState){
        [runLoop run]; 
    /* 
     to keep the thread running till the operation is finished 
     If you don't write this line the control doesn't return to the 
     completion handler of the block. 
     As a result in an array of operations only the first one executes rest don't.

    */
    }
    
}


- (void)operationDidStart
{
    if (![self isCancelled])
    {
        NSURLSessionDownloadTask *downloadTask = [self.urlManager downloadTaskWithRequest:self.request progress:^(NSProgress * _Nonnull downloadProgress)
        {

            NSString *resourceName = [[[self.request URL] absoluteString] lastPathComponent];

            if (downloadProgress.fractionCompleted == 1) {

                NSLog(@"Percentage Completed of %@ : %f",resourceName,downloadProgress.fractionCompleted);
            }
            _progressBlock( resourceName, downloadProgress.fractionCompleted);
        } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response)
        {

//here we specify a path for the resource to be downloaded temporarily and after completion the resource will no longer be at that temp location
            NSString *docpath = [DOC_DIRECTORY stringByAppendingString:@"/doc/"];

            NSURL *docUrl = [[[NSURL alloc] initFileURLWithPath:docpath] URLByAppendingPathComponent:[response suggestedFilename]];
            
            return docUrl;
        }
            completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error){
                if (error == nil) {
                    
    NSString *docpath = [DOC_DIRECTORY stringByAppendingString:@"/doc/"];
                    
     NSString *resourceName = [[[self.request URL] absoluteString] lastPathComponent];

       NSString *downloadPath = [[docpath stringByAppendingPathComponent:resourceName] stringByAppendingPathExtension:@".pdf"];
                                
   NSLog(@"File is downloaded to: %@", filePath);
    //moving the file from temp location to app's own directory
      NSFileManager *fileManager = [NSFileManager defaultManager];
      NSURL *downloadURL = [[NSURL alloc] initFileURLWithPath:downloadPath];
                                
    BOOL fileCopied = [fileManager moveItemAtURL:filePath toURL:downloadURL error:&error];
    NSLog(fileCopied ? @"Yes" : @"No");
                    
        [self finish];
        _successBlock (self.request);
                    
        NSLog(@"Download Completed for : %@",resourceName);
         
                }
                else
                {
                    NSString *resourceName = [[[self.request URL] absoluteString] lastPathComponent];

                    NSLog(@"Failed : %@",resourceName);
                    NSLog(@"download error%@ ",error.description);
                    [self finish];
                    _failureBlock (resourceName);
                    
                }
            }];
        [downloadTask resume];     
        if (self.isCancelled)
            return;
    }
}
//
- (void)finish
{
 self.state = ResourceDownloadOperationFinishedState;
}
//The cancel methods are totally inspired from the AFNetworkingRequestOperation
- (void)cancel
{
    if (![self isFinished] && ![self isCancelled])
    {
        [super cancel];
        
        if ([self isExecuting])
        {
            [self cancelConnection];
        }
    }
}

- (void)cancelConnection
{
    NSDictionary *userInfo = nil;
    
    if(self.request.URL)
    {
        userInfo = [NSDictionary dictionaryWithObject:self.request.URL forKey:NSURLErrorFailingURLErrorKey];
    }
    NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:userInfo];
    
    if (![self isFinished])
    {
        if (self.downloadTask.state == NSURLSessionTaskStateRunning)
        {
            [self.downloadTask cancel];
            self.failureBlock (error.description);
        }
        else
        {
            // Accomodate race condition where `self.connection` has not yet been set before cancellation
            self.error = error;
            [self finish];
        }
    }
}

AFNetworking 3.0 Custom Batch Request Examples

Hi Guys,

If you are like me completely smitten by AFNetworking , then probably you may have noticed that in 3.0 they have removed the batch request using “AFHTTPRequestOperation“. So when I went to migrate my code from 2.0 to 3.0 , I got stuck. I googled , went to stack overflow and then I realised there isn’t a readymade solution to this. So I pulled up my sleeves and started thinking of my own implementation.

My use case was something like this I had a list of documents that I needed to download from the server. And in the meantime I need to show the  progress of the overall download operation to the user so that he can get an idea when the operation is finished and all his documents are now ready to be read.

So in AFNetworking 2.0 I could do like this ,

@interface ViewController ()

@property (nonatomic,strong) NSMutableArray *downloadArray;
@property (nonatomic,strong) NSMutableArray *failedArray;

@property (nonatomic,strong) NSOperationQueue *queue;
@property (nonatomic,strong) UIProgressView *progressView;

@end


-(void) downloadResource
{
    NSMutableArray *allOperations = [[NSMutableArray alloc] init];
    
    if(!_queue)
    {
        _queue = [[NSOperationQueue alloc] init];
    }
    else
    {
        [_queue cancelAllOperations];
    }
    
    [_queue setMaxConcurrentOperationCount:1];

     for (NSString *requestUrlString in self.downloadArray) { 


    NSMutableURLRequest *downloadURLRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestUrlString]];

    AFHTTPRequestOperation* operation = [[AFHTTPRequestOperation alloc] initWithRequest: downloadURLRequest]; 

    [operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long   long totalBytesExpectedToRead) {
    //you can track the progress of individual resources

 }]; 
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { 
     NSMutableURLRequest *request = (NSMutableURLRequest *)operation.request; 
    
   //do whatever you would like to do with the responseObject since this is the resource that you downloaded 

   //you write it to a filePath in document directory 

  //may be reload a table

    NSLog(@"Completed no of operations :  %lu",self.downloadArray.count - _queue.operations.count); 
     if (_queue.operations.count == 0) { 
      [self queueDidFinishSelector:_queue]; 
    } 

   [self.progressView setProgress:(_queue.operationCount/_queue.operations.count) * 100.0]; 

} failure:^(AFHTTPRequestOperation *operation, NSError *error) 
{ 

   NSLog(@"Complete But Failed: %lu",self.downloadArray.count - _queue.operations.count); 

   NSMutableURLRequest *request = (NSMutableURLRequest *)operation.request; 
   [self.failedArray addObject:[request.URL absoluteString]];
       if (_queue.operations.count == 0) {
          [self queueDidFinishSelector:_queue]; 
      } 

     [self.progressView setProgress:(_queue.operationCount/_queue.operations.count) * 100.0]; 

    }]; 

    [allOperations addObject:operation]; 

} 

NSArray *operations = nil; 

     if (allOperations.count) { 
           operations = [AFURLConnectionOperation batchOfRequestOperations:allOperations   progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) { 

 self.progressView.progress = (float)numberOfFinishedOperations/totalNumberOfOperations;

 } completionBlock:^(NSArray *operations) { 

     NSLog(@"All operations in batch complete"); 
     self.progressView.hidden = YES; 
 }]; 

} 

    if (operations) {
     [self.queue addOperations:operations waitUntilFinished:NO]; 
    } 

} 

- (void)queueDidFinishSelector:(NSOperationQueue *)queue 
{ 
     [self.progressView setHidden:YES]; 

   if (self.failedArray.count > 0)
    {
        [self showAlert:@"Sync Failed" error:nil];
    }
    else
    {
        [self showAlert:@"Sync Completed" error:nil];
    }
    
}

-(void)showAlert:(NSString*)title error:(NSString*)err
{
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
    UIAlertView *alert = [[UIAlertView alloc]initWithTitle:title message:err delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
    [alert show];
    
}

But now that we have moved to AFNetworking 3.0 ,  “AFURLConnectionOperation” is no longer available.

So that leaves us to create our own operation.

Firstly replace your old AFNetworking code with the new 3.0 code. You can either use CocoaPods or simply like the good old days , copy the AFNetworking folder to your project folder. As soon as you do it you will realise the following things start showing compilation error.

  1. AFURLConnectionOperation
  2. AFHTTPRequestOperation

After that I am going to show you to rewrite the same piece of code in 3.0.

-(void) downloadResourceUsingNewCode

{

//redundant now
NSMutableArray *allOperations = [[NSMutableArray alloc] init];

if(!_queue)  {
_queue = [[NSOperationQueue alloc] init];
}else{
[_queue cancelAllOperations];
}

[_queue setMaxConcurrentOperationCount:1];

for (NSString *requestUrlString in self.downloadArray) {

NSMutableURLRequest *downloadURLRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestUrlString]];

DownloadOperation *downloadOperation = [[DownloadOperation alloc] initWithRequest:downloadURLRequest successBlock:^(id responseObject)
{

NSMutableURLRequest * request = responseObject; //success block sends back the request

NSLog(@"Completed operation");

} progressBlock:^(id progressMessage, CGFloat progress) {

} failureBlock:^(id errorMessage) {

NSLog(@"failed operation");

}];

[self.queue addOperation:assetDownloadOperation];
}

[self.queue addObserver:self forKeyPath:@"operationCount" options:0 context:NULL];
}

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object

change:(NSDictionary *)change context:(void *)context

{

if (object == self.queue && [keyPath isEqualToString:@"operationCount"])

{

if ([self.queue.operations count] == 0) {

// Do something here when your queue has completed

NSLog(@"queue has completed");

dispatch_async(dispatch_get_main_queue(), ^{

// code here

[self queueDidFinishSelector:self.queue];
});
}
else
{

NSLog(@"Completed no of operations : %lu",self.downloadArray.count - _queue.operations.count);

dispatch_async(dispatch_get_main_queue(), ^{

// code here

self.progressView.progress = (float)(self.downloadArray.count - _queue.operations.count)/self.downloadArray.count;

});

}

} else {

[super observeValueForKeyPath:keyPath ofObject:object

change:change context:context];

}
}

Now the question is what do you write inside your custom operation?

I created my custom operation primarily for the reasons like

1. Put them in a queue and fire them away and there goes your own batch of requests .
2. You get a hold of when all the operations are finished so that you may want to do some UI changes ; in my case it is to hide the progress view.
3. To track which resource is currently getting downloaded and how much progress has it made so that in a tableview of list of resources I can add a progress view to show its individual progress and when it finishes I can reload the table and show the progress going on in the next resource.

Now to fulfil all these, in each operation I created a “NSURLSessionDownloadTask” which will download the resource that I intend to download. If the request returns success response then move the file to the desired file path.

If you want to know how to create your own custom operation , please refer to my next post as this one is getting too long.

Let me know if you have also implemented the batch operations or requests with AFNetworking and let’s find out the best approach.