Operación síncrona de AFNetworking en NSOperationQueue en iPhone

My app is working this way : - create an album and take pictures - send them on my server - get an answer / complementary information after picture analysis.

I have some issue with the sending part. Here is the code

@interface SyncAgent : NSObject <SyncTaskDelegate>

@property  NSOperationQueue* _queue;
-(void)launchTasks;

@end


@implementation SyncAgent

@synthesize _queue;

- (id)init
{
    self = [super init];
    if (self) {
        self._queue = [[NSOperationQueue alloc] init];
        [self._queue setMaxConcurrentOperationCount:1];
    }
    return self;
}

-(void) launchTasks {

    NSMutableArray *tasks = [DataBase getPendingTasks];

    for(Task *t in tasks) {
        [self._queue addOperation:[[SyncTask alloc] initWithTask:t]];
    }
}
@end

and the SyncTask :

@interface SyncTask : NSOperation

@property (strong, atomic) Task *_task;
-(id)initWithTask:(Task *)task;
-(void)mainNewID;
-(void)mainUploadNextPhoto:(NSNumber*)photoSetID;

@end

@implementation SyncTask

@synthesize _task;

-(id)initWithTask:(Task *)task {
    if(self = [super init]) {
        self._task = task;
    }
    return self;
}

-(void)main {

    NSLog(@"Starting task : %@", [self._task description]);
    // checking if everything is ready, sending delegates a message etc

    [self mainNewID];
}

-(void)mainNewID {

    __block SyncTask *safeSelf = self;

    [[WebAPI sharedClient] createNewPhotoSet withErrorBlock:^{
        NSLog(@"PhotoSet creation : error")
    } andSuccessBlock:^(NSNumber *photoSetID) {
        NSLog(@"Photoset creation : id is %d", [photoSetID intValue]);
        [safeSelf mainUploadNextPhoto:photoSetID];
    }];
}

-(void)mainUploadNextPhoto:(NSNumber*) photoSetID {

    //just admit we have it. won't explain here how it's done
    NSString *photoPath;

    __block SyncTask *safeSelf = self;

    [[WebAPI sharedClient] uploadToPhotosetID:photoSetID withPhotoPath:photoPath andErrorBlock:^(NSString *photoPath) {
        NSLog(@"Photo upload error : %@", photoPath);

    } andSuccessBlock:^(NSString *photoPath) {

        NSLog(@"Photo upload ok : %@", photoPath);
        //then we delete the file
        [safeSelf mainUploadNextPhoto:photoSetID];
    }];
}
@end

Every network operations are done using AFNetworking this way :

-(void)myDummyDownload:(void (^)(NSData * data))successBlock
{
    AFHTTPClient* _httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:@"http://www.google.com/"]];
    [_httpClient registerHTTPOperationClass:[AFHTTPRequestOperation class]];

    NSMutableURLRequest *request = [_httpClient requestWithMethod:@"GET" path:@"/" nil];
    [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];

    AFHTTPRequestOperation *operation = [_httpClient HTTPRequestOperationWithRequest:(NSURLRequest *)request
        success:^(AFHTTPRequestOperation *operation, id data) {
            if(dataBlock)
                dataBlock(data);
        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            NSLog(@"Cannot download : %@", error);
    }];

    [operation setShouldExecuteAsBackgroundTaskWithExpirationHandler:^{
        NSLog(@"Request time out");
    }];

    [_httpClient enqueueHTTPRequestOperation:operation];
}

My problem is : my connections are made asynchronously, so every task are launched together without waiting fo the previous to finish even with [self._queue setMaxConcurrentOperationCount:1] in SyncAgent.

Do I need to perform every connection synchronously ? I don't think this is a good idea, because a connection never should be done this way and also because I might use these methods elsewhere and need them to be performed in background, but I cannot find a better way. Any idea ?

Oh and if there is any error/typo in my code, I can assure you it appeared when I tried to summarize it before pasting it, it is working without any problem as of now.

Gracias !

PS: Sorry for the long pastes I couldn't figure out a better way to explain the problem.

EDIT: I found that using a semaphore is easier to set up and to understand : ¿Cómo espero a que finalice un bloque enviado de forma asincrónica?

preguntado el 04 de julio de 12 a las 09:07

2 Respuestas

If you have any control over the server at all, you should really consider creating an API that allows you to upload photos in an arbitrary order, so as to support multiple simultaneous uploads (which can be quite a bit faster for large batches).

But if you must do things synchronized, the easiest way is probably to enqueue new requests in the completion block of the requests. i.e.

// If [operations length] == 1, just enqueue it and skip all of this
NSEnumerator *enumerator = [operations reverseObjectEnumerator];
AFHTTPRequestOperation *currentOperation = nil;
AFHTTPRequestOperation *nextOperation = [enumerator nextObject]; 
while (nextOperation != nil && (currentOperation = [enumerator nextObject])) {
  currentOperation.completionBlock = ^{
    [client enqueueHTTPRequestOperation:nextOperation];
  }
  nextOperation = currentOperation;
}
[client enqueueHTTPRequestOperation:currentOperation];

Respondido 04 Jul 12, 15:07

unfortunately this doesn't work. The NSOperation launching the batch upload will still return before it has been uploaded. I would need a method for upload that returns only when the upload is finished/failed - dvkch

Then I misunderstood your requirements. Use enqueueBatchOfHTTPRequestOperationsWithRequests:progressBlock:completionBlock: - Mattt

indeed this could be a way, but it will still not work unless I create a method creating the ID on the server, and then uploading every file. As of now I was using my SyncTask to do them in the right order only if needed and I prefer not to do it in my Web class to keep it very flexible. and using your way my SyncAgent will still "think" every SyncTask (NSOperation) have been finished. - dvkch

still cannot find a way to do so. I have tried -waitUntilFinished but it seems to wait for the connection timeout even if it was successful. weird. Any idea ? - dvkch

Alejate de -waitUntilFinished in general. Perhaps one thing to keep in mind is that operations are marked as finished when network requests finish loading--anything in the completion block will execute afterwards regardless. - Mattt

The following code works for me, but I am not sure of the drawbacks. Take it with a pinch of salt.

 - (void) main {

  NSCondition* condition = [[NSCondition alloc] init];  
  __block bool hasData = false;
  [condition lock];

  [[WebAPI sharedClient] postPath:@"url" 
                        parameters:queryParams 
                           success:^(AFHTTPRequestOperation *operation, id JSON) {
                             //success code
                             [condition lock];
                             hasData = true;
                             [condition signal];
                             [condition unlock];

                           } 
                           failure:^(AFHTTPRequestOperation *operation, NSError *error) {

                             //failure code

                             [condition lock];
                             hasData = true;
                             [condition signal];
                             [condition unlock];
                           }];

  while (!hasData) {
    [condition wait];
  }
  [condition unlock];

}

Respondido 12 Jul 12, 09:07

I must confess I think about it. This seams a little bit hacky but I think I'll do it anyway. I was thinking about using dispatch_syncto do it properly, have a single queue for all my uploads but this solution seams easier. - dvkch

You should replace NSCondition with GCD dispatch_groups. Mainly, look at dispatch_group_enter, dispatch_group_wait y dispatch_group_leave. Mike Ash has un buen tutorial. - arenas de marca

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.