4

Suppose that I use another SDK (which I do not have control of) with an API that imports 1 file asynchronously, and calls a completion callback on completion. The following is an example API.

func importFile(filePath: String, completion: () -> Void)

I need to import 10 files (one by one) using this API, but I need it to be cancellable, e.g. after Files 1,2,3 has been successfully imported, while File 4 is being imported, I want to be able to cancel the whole set of operations (import of the 10 Files), such that File 4 will finish (since it already started), but Files 5-10 will not be imported anymore.

In addition, I also need to report progress of the import. When File 1 has been imported successfully, then I should report progress of 10% (1 out of 10 has been finished).

How can I achieve this?

I am considering using NSOperationQueue with 10 NSOperations, but it seems that progress reporting will be difficult.

Dj S
  • 9,992
  • 1
  • 18
  • 24

3 Answers3

1

So, I believe that this is the following you want from your question:

  1. Import n files one by one in a queue
  2. Report progress when each file is imported
  3. Ability to cancel operation in the middle

It can be achieved using NSOperationQueue and NSBlockOperation in the following way.

  1. Create AsyncBlockOperation and NSOperationQueue+AsyncBlockOperation classes from code given in answer for the following StackOverflow question: NSOperation wait until asynchronous block executes
  2. Import both the classes into the file they are going to be used
  3. Create an operation queue

    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    operationQueue.maxConcurrentOperationCount = 1;
    operationQueue.name = @"com.yourOrganization.yourProject.yourQueue";
    
  4. Create a function which gives you a callback for getting progress

    - (void)importFilesFromFilePathArray:(NSArray *)pathsArray
                        inOperationQueue:(NSOperationQueue *)operationQueue
                            withProgress:(void (^)(CGFloat progress))progressBlock {
    
      }
    
  5. Inside the function defined in 2, use NSBlockOperation for performing your operations in the NSOperationQueue

    for (int i = 0; i < pathsArray.count; i++) {
    
        [operationQueue addAsyncOperationWithBlock:^(dispatch_block_t completionHandler) {
            [self importFile:(NSString *)[pathsArray objectAtIndex:i] completion:^(bool completion) {
                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    CGFloat progress = (100 * (float)(i+1)/pathsArray.count);
                    progressBlock(progress);
                    if (progress == 100) {
                       successBlock(YES);
                    }
                 }];
                 completionHandler();
             }];
         }];
    }
    
  6. For cancelling operations, we can simply use the operationQueue that we created in the 1st step

    [operationQueue cancelAllOperations];
    

I tried this code myself. It's working well. Feel free to suggest improvements to make it better :)

Community
  • 1
  • 1
KrishnaCA
  • 5,069
  • 1
  • 18
  • 30
  • Looks good. But what will you do if *importFile* is asynchronous, as stated in the problem description? And, how about the completion callback when all operations have been completed, how do you address that requirement? – Dj S Nov 09 '16 at 07:15
  • I'll add that in a few minutes – KrishnaCA Nov 09 '16 at 08:13
  • If you get a progress of 100, then that means it is successful right. Here, failure reporting becomes difficult when you have an option to manually cancel the operations. Small doubt: If one of the files importing gets failed, should we continue or should we cancel all the operations? – KrishnaCA Nov 09 '16 at 08:25
  • Also, since your `importFile` function is asynchronous, you can use https://gist.github.com/msealand/b90e621ad5420bc4f2bf instead of normal NSOperationBlock – KrishnaCA Nov 09 '16 at 13:33
  • I see, so you are suggesting an asynchronous type of implementation for a subclass of NSOperation. Thanks! – Dj S Nov 11 '16 at 02:30
  • I edited my answer in such a way that it answers all your queries. Please try it. – KrishnaCA Nov 12 '16 at 21:13
0

NSOperationQueue offers a nice object oriented abstaraction and is the way I would go.

  • Create a NSOperationQueuenamed importQueue

For each import:

  • Create a NSOperation
  • Store the operation to reach and possibly cancle it later on, for instance in a dictionary [ImportNumber:NSOperation]
  • Add the operation to importQueue

In regards to the progress state:

Option 1:

NSOperationQueue has a property called operations, whichs count you are able to observe. (importQueue.operations.count).

Option 2:

NSOperation offers completion blocks. You can increase a counter when queueing the operation and decrease it within the completion block or when cancelling.


Further reading: Apple documentation, asynchronous-nsoperation-why-and-how, nice but oldish tutorial.

shallowThought
  • 16,998
  • 6
  • 55
  • 100
  • 1
    How will you implement NSOperation for an asynchronous API (as stated in the problem description)? – Dj S Nov 09 '16 at 07:17
  • A concrete implementation would be too long and imo. SO is not meant to offer this. If you have questions after reading the tutorial, let us know, maybe in a new question for specific new issues you are running into. – shallowThought Nov 09 '16 at 10:44
  • Sorry, I'm not asking for a concrete implementation. A brief answer is okay. The question explicitly dictates that the API (from a 3rd party SDK) is asynchronous, and your answer uses a synchronous API. – Dj S Nov 10 '16 at 02:28
  • An NSOperation can run synchronous (or non-concurrent) and asynchronous (or concurrent). All well explained well in the mentioned tutorial. – shallowThought Nov 10 '16 at 09:28
  • The NSOperations (ImageDownloader and ImageFiltration) in the tutorial you mentioned use synchronous APIs only. What asynchronous APIs are you referring to? – Dj S Nov 11 '16 at 02:25
  • I added more links. Hope that helps. – shallowThought Nov 11 '16 at 10:37
0

I think you should add dependencies on your operation. The idea is this:

1Op = NSOperation..
2Op = NSOperation..
.
.
10Op = NSOperation..

10Op.addDependency(9Op)
9Op.addDependency(8Op) and so on...

Then your operation subclass should override the cancel method in this way

cancel() {
    super.cancel()
    for dep in dependencies {
        dep.cancel()
    }
}
iGenio
  • 379
  • 1
  • 6