Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Looping and asynchronous connections in objective-c

I have an array of table names and wish to loop through them and take their name and append a URL to create a connection to a web service to download JSON data. The loop appears to work at first, each of the three table names in the array are taken and used to create a connection to a web service and the data is downloaded but when the loop has finished (gone from 3 to 0) The loop appears to start up again and loop infinitely for the last two tables in the array.

Log output (notice that speaker and exhibitor are repeated over and over):

2013-12-16 10:38:08.755 WebServiceTest[501:60b] loopCount = 3
2013-12-16 10:38:08.758 WebServiceTest[501:60b] table name = workshop
2013-12-16 10:38:08.817 WebServiceTest[501:60b] LoopCount is: 2 
2013-12-16 10:38:08.821 WebServiceTest[501:60b] loopCount = 2
2013-12-16 10:38:08.822 WebServiceTest[501:60b] table name = exhibitor
2013-12-16 10:38:08.827 WebServiceTest[501:60b] LoopCount is: 1
2013-12-16 10:38:08.830 WebServiceTest[501:60b] loopCount = 1
2013-12-16 10:38:08.831 WebServiceTest[501:60b] table name = speaker
2013-12-16 10:38:08.835 WebServiceTest[501:60b] LoopCount is: 0
2013-12-16 10:38:09.199 WebServiceTest[501:3707] Status code = 200
2013-12-16 10:38:09.204 WebServiceTest[501:3707] Objects in current table - speaker = 1
2013-12-16 10:38:09.207 WebServiceTest[501:3707] Status code = 200
2013-12-16 10:38:09.210 WebServiceTest[501:3707] Objects in current table - exhibitor = 2
2013-12-16 10:38:09.229 WebServiceTest[501:450b] Status code = 200
2013-12-16 10:38:09.234 WebServiceTest[501:450b] Objects in current table - speaker = 1
2013-12-16 10:38:09.240 WebServiceTest[501:3707] Status code = 200
2013-12-16 10:38:09.244 WebServiceTest[501:3707] Objects in current table - exhibitor = 2
2013-12-16 10:38:09.271 WebServiceTest[501:450b] Status code = 200
2013-12-16 10:38:09.274 WebServiceTest[501:450b] Objects in current table - speaker = 1
2013-12-16 10:38:09.294 WebServiceTest[501:450b] Status code = 200
2013-12-16 10:38:09.298 WebServiceTest[501:4803] Status code = 200
2013-12-16 10:38:09.302 WebServiceTest[501:4803] Objects in current table - exhibitor = 2
2013-12-16 10:38:09.309 WebServiceTest[501:4803] Status code = 200
2013-12-16 10:38:09.337 WebServiceTest[501:4803] Objects in current table - speaker = 1
2013-12-16 10:38:09.338 WebServiceTest[501:4803] Status code = 200
2013-12-16 10:38:09.341 WebServiceTest[501:4803] Objects in current table - exhibitor = 2
2013-12-16 10:38:09.347 WebServiceTest[501:4803] Status code = 200
2013-12-16 10:38:09.352 WebServiceTest[501:4803] Objects in current table - speaker = 1
2013-12-16 10:38:09.311 WebServiceTest[501:450b] Objects in current table - workshop = 143

viewController.h:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController<UITableViewDataSource, UITableViewDelegate, NSURLConnectionDataDelegate>{
NSMutableArray *arrayTable;
}

@property (weak, nonatomic) IBOutlet UITableView *myTableView;
@property NSMutableArray *tables;
@property NSMutableArray *tableNames;
@property NSMutableURLRequest *request;
@property NSString *tableName;

-(void) startUpdate;
typedef void(^completion_t)(NSArray* objects, NSError*error);
-(void)fetchData:(NSString *)tableName
   withCompletion:(completion_t)completionHandler;
-(void)fetchObjectsWithTableName:(NSString*)tableName
                  completion:(completion_t)completionHandler;

viewController.m:

#import "ViewController.h"
@implementation ViewController

- (void)viewDidLoad
{
    [self startUpdate];
    [super viewDidLoad];
    [[self myTableView]setDelegate:self];
    [[self myTableView]setDataSource:self];
    arrayTable =[[NSMutableArray alloc]init];
}

-(void)startUpdate
{
    NSArray* tableNames =  @[@"speaker", @"exhibitor", @"workshop"]; 

    NSUInteger loopCount = tableNames.count;
    while (loopCount > 0){
        NSString *tableName = [tableNames objectAtIndex:loopCount -1];
        NSLog(@"loopCount = %lu", (unsigned long)loopCount);
        NSLog(@"table name = %@", tableName);

        [self fetchObjectsWithTableName:[tableName mutableCopy] completion:^(NSArray* objects, NSError*error){
            if (error) {
                NSLog(@"Error: %@", error);
            } else {
                NSLog(@"Result: %@", objects);
            }
        }];
        loopCount --;
        NSLog(@"LoopCount is: %i", loopCount);
    }
}

-(void)fetchObjectsWithTableName:(NSString*)tableName
                  completion:(completion_t)completionHandler;
{
    [self fetchData:tableName withCompletion:^(NSArray* objects, NSError*error){
        if (objects) {
            [self.tables addObject:objects];
            [self fetchObjectsWithTableName:tableName completion:completionHandler];
        }
        else if (objects == nil){
            NSLog(@"objects = %@", objects);
            NSLog(@"break out of FOWTN");
        } else {
            NSLog(@"Objects is something else...");
        }
    }];
    if (completionHandler) {
        completionHandler(self.tables, nil);
    }
}

-(void)fetchData:(NSString *)tableName
                    withCompletion:(completion_t)completionHandler
{
    NSString *currentURL = [NSString stringWithFormat:@"https://testapi.someURL.com/api/congress/%@", tableName];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:currentURL]];
    [request addValue:@"application/json" forHTTPHeaderField:(@"Accept")];
    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[[NSOperationQueue alloc] init]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
     {
         NSError* err = error;
         NSArray* objects; // final result array as a representation of JSON Array
         if (response) {
             NSHTTPURLResponse *newResp = (NSHTTPURLResponse*)response;
             if (newResp.statusCode == 200) {
                 NSLog(@"Status code = %li", (long)newResp.statusCode);
                 if ([data length] >0 && error == nil)
                 {
                     NSError* localError;
                     objects = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
                     if (objects) {
                         if (completionHandler) {
                             completionHandler(objects, nil);
                         }
                         //NSLog(@"Objects in current table - %@ = %@", tableName, objects);
                         NSLog(@"Objects in current table - %@ = %lu", tableName, (unsigned long)objects.count);    
                         return;
                     } else {
                         err = localError;
                     }
                 } else {
                    // err = ...
                 }
             }
         }
         if (objects == nil) {
             NSLog(@"Nothing found in table: %@", tableName);
             //assert(err);
             if (completionHandler) {
                 completionHandler(nil, err);
             }
         }
    }];
}
like image 255
Agamemnon Avatar asked Dec 16 '13 10:12

Agamemnon


1 Answers

(Edit: deleted)

IMHO, the loop looks better with a for loop, iterating in the "right" direction:

-(void)startUpdate
{
    NSUInteger count = tableNames.count;
    for (int i = 0; i < count; ++i){
        NSString *tableName = [tableNames objectAtIndex:i];
        ...
    }
}

A few additional suggestions:

Having said this and fixed this issue, now, you need to realize that you are invoking asynchronous methods within the loop's block. Hence your startUpdate method becomes itself asynchronous as well!

If you want that the call-site gets notified when all asynchronous methods have been finished, your startUpdate may use a completion handler:

- (void) startUpdateWithCompletion:(completion_t)completionHandler;

You can invoke asynchronous methods in a for loop. But effectively, this will process all asynchronous tasks in parallel, unless the underlying asynchronous tasks take care itself through executing on a private shared queue whose "size" (number of simultaneous operations) is configurable.

A suitable concrete implementation now depends on your requirements, namely whether you need to control the number of simultaneous running tasks and where you want to accomplish this. It also depends whether you want to be able to cancel the loop from any other thread at any time, if that should be necessary.

For example:

Suppose, we don't make any assumptions about the underlying asynchronous tasks, which means, they will not use a shared private queue in order to limit the number of simultaneous running tasks by itself. Furthermore, you want to ensure that all tasks run one after the other - or serial.

One approach (out of severals) is to use a NSOperationQueue, set its maximum number of operation to one, and add your asynchronous tasks which need to be wrapped into a "concurrent" NSOperation.

A "concurrent" NSOperation is an operation which needs to be started with the start method, which is asynchronous. Basically, the operation will finish when your asynchronous task is complete. When added to a NSOperationQueue, the queue takes care when to start the operation.

Unfortunately, subclassing a NSOperation as a concurrent operation requires to take care of a few subtleties. The official documentation Subclassing Notes, doesn't describe them in great detail, thus you may take a look here into this code snippet: Canonical Implementation of a Subclass of NSOperation.

Now, lets suppose you have a properly subclass of a NSOperation, call it FetchTableOperation:

completion_t completionHandler = ^(..) {..};
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperations = 1;
for (NSString tableName in self.tableNames) {
    FetchTableOperation* op = 
       [[FetchTableOperation alloc] initWithName:tableName 
                                     completion: ^{...}];
    [queue addOperation:op];
}

In order to get notified when the operations are finished add a "sentinel" block:

[queue addOperationWithBlock:^{ 
    // finished
}];

Caveat:

  • You need to create a proper subclass of a concurrent NSOperation to wrap your asynchronous method.

  • You get notified when the last operation finished, and NOT when the last operation's completion block finished!

  • The completion handlers may still execute in parallel! (unless they are executed on the main thread or on a private queue whose size equals one)

  • All tasks will be enqueued - which isn't an issue, unless the number of tasks is really large. Each enqueued task will simply consume a little system RAM.

The advantage using an NSOperationQueue is that you can cancel pending operations at any time. Additionally, you can adjust the size of the queue, that is the number of max concurrent operations quite easily with the property maxConcurrentOperations.

Other Approaches:

Using a dispatch_group Running all tasks simultaneously

In contrast, this is a quick and easy solution. However, your tasks will be started all in parallel:

dispatch_group group = dispatch_group_create();
for (NSString* tableName in self.tableNames) {
    dispatch_group_enter(group);
    [self fetchObjectsWithTableName:tableName completion:^{
        ...
        dispatch_group_leave(group);
    }];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    ... // all tasks *and* all completion handler finished
});

"Asynchronous Loop" which runs asynchronous tasks sequentially:

This is also a quite simple solution - once you understand the pattern.

How to download multiple images asynchronously in iOS without effect on UI?

Using a NSArray Category which sequentially invokes asynchronous tasks:

This is a "reusable" component, which can be utilized easily - once you implemented it. You can use it as follows:

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

unary_async_t task = ^(id input, completion_t completionHandler)
{
    [self fetchObjectsWithTableName:input completion:^(NSData* result, NSError*error){
         if (error == nil) {
             ... ;
         }
         completionHandler(error ? error : @"OK");
    }];  
};

NSArray* tableNames =  @[@"speaker", @"exhibitor", @"workshop"]; 

[tableNames forEachApplyTask:task completion:^(id result){
    // result is an array containing the result of each operation in the same order
    ...
}];

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

like image 113
CouchDeveloper Avatar answered Oct 13 '22 10:10

CouchDeveloper