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];
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *