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