Tar_Tw45
Tar_Tw45

Reputation: 3222

Remove object from iterating NSMutableArray

According to Best way to remove from NSMutableArray while iterating?, we can't remove an object from NSMutableArray while iterating, yes.

But, what if I have a code like the following

- (void)sendFeedback {
    NSMutableArray *sentFeedback = [NSMutableArray array];
    for (NSMutableDictionary *feedback in self.feedbackQueue){
        NSURL *url = [NSURL URLWithString:@"someApiUrl"];
        ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
        [request setPostValue:[feedback objectForKey:@"data"] forKey:@"data"];
        [request setCompletionBlock:^{
            [sentFeedback addObject:feedback];
        }];
        [request startAsynchronous];
    }
    [self.feedbackQueue removeObjectsInArray:sentFeedback];
}

I'm using a NSRunLoop to create a NSThread to execute the sendFeedback method every a period of time. The way I sent data to the API is by using Asynchronous method (which will create a background thread for each request) Once the feedback has been sent, it has to be removed before NSRunner execute this method at the next period to avoid duplicate data submission.

By using asynchronous, the loop (the main thread) will continue running without waiting for the response from server. In some cases (maybe most cases), the loop will finish running before all the response from server of each request come back. If that so, the completion block's code will be execute after the removeObjectsInArray which will result in sent data remains in self.feedbackQueue

I'm pretty sure that there are several ways to avoid that problem. But the only one that I can think of is using Synchronous method instead so that the removeObjectsInArray will not be execute before all the request's response are come back (Either success or fail). But if I do so, it's mean that the internet connection has to be available for longer period. The time needed to the sendFeedback's thread will be longer. Even it will be run by newly created NSThread which will not cause the app to not respond, resources will be needed anyways.

So, is there any other way besides the one I mentioned above? Any suggestion are welcome.

Thank you.

Upvotes: 1

Views: 610

Answers (2)

danh
danh

Reputation: 62686

One approach is to keep track of the requests inflight and do the queue clean up when they are all done. Keeping track with the block is a little tricky because the naive approach will generate a retain cycle. Here's what to do:

- (void)sendFeedback {

    NSMutableArray *sentFeedback = [NSMutableArray array];

    // to keep track of requests
    NSMutableArray *inflightRequests = [NSMutableArray array];

    for (NSMutableDictionary *feedback in self.feedbackQueue){
        NSURL *url = [NSURL URLWithString:@"someApiUrl"];

        ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];

        // save it
        [inflightRequests addObject:request];

        // this is the ugly part. but this way, you can safely refer
        // to the request in it's block without generating a retain cycle
        __unsafe_unretained ASIFormDataRequest *requestCopy = request;

        [request setPostValue:[feedback objectForKey:@"data"] forKey:@"data"];
        [request setCompletionBlock:^{
            [sentFeedback addObject:feedback];

            // this one is done, remove it
            // notice, since we refer to the request array here in the block,
            // it gets retained by the block, so don't worry about it getting released
            [inflightRequests removeObject:requestCopy];

            // are they all done?  if so, cleanup
            if (inflightRequests.count == 0) {
                [self.feedbackQueue removeObjectsInArray:sentFeedback];
            }
        }];
        [request startAsynchronous];
    }
    // no cleanup here.  you're right that it will run too soon here
}

Upvotes: 1

Jason Coco
Jason Coco

Reputation: 78343

There are a few ways to deal with this kind of problem. I suggest using a dispatch group to synchronize your feedback and using an instance variable to keep from executing a new feedback batch while one is still in progress. For this example, let's assume you create an instance variable named _feedbackUploadInProgress to your class, you could rewrite your -sendFeedback method like this:

- (void)sendFeedback
{
  if( _feedbackUploadInProgress ) return;
  _feedbackUploadInProgress = YES;

  dispatch_group_t group = dispatch_group_create();
  NSMutableArray *sentFeedback = [NSMutableArray array];
  for (NSMutableDictionary *feedback in self.feedbackQueue) {
    // enter the group for each item we're uploading
    dispatch_group_enter(group);
    NSURL *url = [NSURL URLWithString:@"someApiUrl"];
    ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
    [request setPostValue:[feedback objectForKey:@"data"] forKey:@"data"];
    [request setCompletionBlock:^{
      [sentFeedback addObject:feedback];
      // signal the group each time we complete one of the feedback items
      dispatch_group_leave(group);
    }];
    [request startAsynchronous];
  }
  // this next block will execute on the specified queue as soon as all the
  // requests complete
  dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    [self.feedbackQueue removeObjectsInArray:sentFeedback];
    _feedbackUploadInProgress = NO;
    dispatch_release(group);
  });
}

Upvotes: 4

Related Questions