Asynchronous Recursive Calls using Box sdk - How to iterate all files in using iOS BOX sdk api

StackOverflow https://stackoverflow.com/questions/20495920

Frage

I am trying to iterate all images stored in a Box account.

The following code isn't correct and can't seem to find the error.

The problem seems to be waiting for all of the asynchronous recursive calls to complete in order to know when there are no more images to be fetched in order to signal completion

-(void)enumerateFiles
{
    [self recursiveEnumerateFilesAtPath:@"0" completion:nil];
}


-(void)recursiveEnumerateFilesAtPath:(NSString *)folderID completion:(void(^)())block
{
    static NSArray *imageExtensions;
    imageExtensions = @[@"jpg",@"jpeg",@"png"];
    [self enumerateFilesAtPath:folderID completion:^(BoxCollection *collection) {
        NSUInteger numberOfItems = collection.numberOfEntries;
        for (int i = 0; i < numberOfItems; i++) {
            id model = [collection modelAtIndex:i];

            if ([model isKindOfClass:[BoxFolder class]])
            {
                BoxFolder *folder = (BoxFolder *)model;
                dispatch_group_t group = dispatch_group_create();
                dispatch_group_enter(group);
                [self recursiveEnumerateFilesAtPath:folder.modelID completion:^{
                    dispatch_group_leave(group);
                }];
                dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
            } else if ([model isKindOfClass:[BoxItem class]])
            {
                BoxItem *item = (BoxItem *)model;
                NSString *extension = [[item.name pathExtension] lowercaseString];
                if ([imageExtensions containsObject:extension])
                {
                    [self.items addObject:model];
                }

            }
        }
        if (block)
        {
            block();
        }
    }];
}
-(void)enumerateFilesAtPath:(NSString *)folderID completion:(void(^)(BoxCollection *collection))block
{
    BoxCollectionBlock success = ^(BoxCollection *collection)
    {
        block(collection);
    };

    BoxAPIJSONFailureBlock failure = ^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, NSDictionary *JSONDictionary)
    {
        block(nil);
    };

    [[BoxSDK sharedSDK].foldersManager folderItemsWithID:folderID requestBuilder:nil success:success failure:failure];
}
War es hilfreich?

Lösung

The key to a viable approach is to make the inner loop, that is

for (int i = 0; i < numberOfItems; i++) {..}

a construct which sequentially invokes a number of asynchronous tasks and is itself asynchronous. The asynchronous task should be a block (^) which will invoke the outer method and will capture state so that its outer method recursiveEnumerateFilesAtPath: can be invoked in a recursive manner.

Note that this is not a recursion we know from functions, where a function calls itself before it returns, and where the state is hold on the program stack. Here, we rather have an iteration (asynchronous loop) - and there's no state on the stack, instead the state required for the "recursion" is hold in the variables captured in the blocks, and these reside on the heap.

That is, you need to transform the "for loop" (for (int i = 0; i < numberOfItems; i++) {..}) into some asynchronous method, e.g:

- (void) forEach:(NSArray*)objects applyTask:(task_t) completion:(completion_t)completionHandler;

where task is asynchronous, has a completion handler, and (conditionally) calls the outer method recursiveEnumerateFilesAtPath:.

This forEach construct could be implemented using a NSOperationQueue, whose max concurrent operations is set to one, and where the task is a NSOperation with a completion handler. Alternatively - and simpler in this case, it can be implemented using dispatch queues and a dispatch group. Or - even more simpler, it can be implemented using an "asynchronous loop". I'll show a possible implementation, see "NSArray Category forEachApplyTask:completion: below.

Now, suppose there is a Category for NSArray with an asynchronous forEachApplyTask:completion: method:

@interface NSArray (AsyncExtension)
- (void) forEachApplyTask:(unary_async_t)task completion:(completion_t) completion;
@end

This method takes a block of type unary_async_t as first parameter:

typedef void (^unary_async_t)(id input, completion_t completion);

and a completion handler as second parameter:

typedef void (^completion_t)(id result);

This typedef of a completion handler, is generic and can be used for all completion handler variants.

Let's suppose, the block task will be sequentially applied for each element in the receiver (the array). And suppose, this array is an array of "model"s in your original code -- you just need to create an array of "model"s out from the BoxCollection object.

When finished, the completion handler passes a result, which is an array containing the result of each task in the same order.

This task executes the block ({..}) in your former for loop.

Your method recursiveEnumerateFilesAtPath: will become inevitable asynchronous (since to invokes an asynchronous method in its implementation), and thus gets a completion handler parameter as well.

With the given assumptions made above, you can implement your problem as shown below:

-(void)recursiveEnumerateFilesAtPath:(NSString *)folderID 
                          completion:(completion_t)finalCompletionHandler 
{
    [self enumerateFilesAtPath:folderID completion:^(BoxCollection *collection) {
        NSArray* models = ...; // create an NSArray from the collection
        [models forEachApplyTask:^(id model, completion_t taskCompletionHandler) {
            if ([model isKindOfClass:[BoxFolder class]]) {
                BoxFolder *folder = (BoxFolder *)model;
                [self recursiveEnumerateFilesAtPath:folder.modelID completion:^(id result){
                    // result should be @"finished folder"
                    taskCompletionHandler(@"folder"); // taskCompletionHandler will never be nil
                }];
            } 
            else if ([model isKindOfClass:[BoxItem class]]) {
                BoxItem *item = (BoxItem *)model;
                NSString *extension = [[item.name pathExtension] lowercaseString];
                if ([imageExtensions containsObject:extension]) {
                    [self.items addObject:model];
                }
                taskCompletionHandler(@"item");
            }
        } 
        completion:^(id array){
            // this folder is finished. array may be for example @[@"folder", @"item", @"item"]          
            if (finalCompletionHandler) {
                finalCompletionHandler(@"finished folder");
            }
        }];
    }];
}

Caution: not tested, you may experience issues!

Note, that everything runs asynchronous, and that there is no recursion whose state is put on the stack. The required state for recursively iterating the folders is put on the heap, enclosed in captured variables in the block.


The NSArray Category method forEachApplyTask:completion: is a reusable component, which can be utilized in many cases. One can implement it as follows:

Implementation of Category NSArray

/**
 Objective:

 Asynchronously transform or process an array of items - one after the
 other and return the result of each transform in an array.

 Synopsis:

 void transform_each(NSArray* inArray, unary_async_t task, completion_t completion);

 */


#import <Foundation/Foundation.h>
#import <dispatch/dispatch.h>

/**
 Typedef of a generic completion handler for an asynchronous task.

 The parameter _result_ is the eventual result of the asynchronous
 task. In case the task has been failed _result_ SHALL be an
 NSError object.
 */
typedef void (^completion_t)(id result);


/**
 Typedef for an asynchronous "transform" function. It's an
 unary block taking an input as parameter and signals
 the eventual result via a completion handler.
 */
typedef void (^unary_async_t)(id input, completion_t completion);


/**
 `transform_each` sequentially applies an asynchronous transform function
 to each object in the input array _inArray_ and signals the result as an
 array containing the result of each transform applied to the input object.

 Function `transform_each` is itself a asynchronous function, that is,
 its eventual result will be signaled to the client through a completion
 handler.

 The result array contains the transformed objects in order of the
 corresponding input array.
 */
void transform_each(NSArray* inArray, unary_async_t task, completion_t completion);


// implementation


static void do_each(NSEnumerator* iter, unary_async_t task, NSMutableArray* outArray, completion_t completion)
{
    id obj = [iter nextObject];
    if (obj == nil) {
        if (completion)
            completion([outArray copy]);
        return;
    }
    task(obj, ^(id result){
        [outArray addObject:result];
        do_each(iter, task, outArray, completion);
    });
}

void transform_each(NSArray* inArray, unary_async_t task, completion_t completion) {
    NSMutableArray* outArray = [[NSMutableArray alloc] initWithCapacity:[inArray count]];
    NSEnumerator* iter = [inArray objectEnumerator];
    do_each(iter, task, outArray, completion);
}


/*******************************************************************************
    Example
 *******************************************************************************/

// A Category for NSArray

@interface NSArray (AsyncExtension)
- (void) async_forEachApplyTask:(unary_async_t) task completion:(completion_t) completion;
@end

@implementation NSArray (AsyncExtension)
- (void) async_forEachApplyTask:(unary_async_t) task completion:(completion_t) completion {
    transform_each(self, task, completion);
}
@end

See also: https://gist.github.com/couchdeveloper/6155227

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top