What makes a completion handler execute the block when your task of interest is complete?
Asked Answered
S

2

3

I have been asking and trying to understand how completion handlers work. Ive used quite a few and I've read many tutorials. i will post the one I use here, but I want to be able to create my own without using someone else's code as a reference.

I understand this completion handler where this caller method:

-(void)viewDidLoad{
[newSimpleCounter countToTenThousandAndReturnCompletionBLock:^(BOOL completed){
        if(completed){ 
            NSLog(@"Ten Thousands Counts Finished");
        }
    }];
}

and then in the called method:

-(void)countToTenThousandAndReturnCompletionBLock:(void (^)(BOOL))completed{
    int x = 1;
    while (x < 10001) {
        NSLog(@"%i", x);
        x++;
    }
    completed(YES);
}

Then I sorta came up with this one based on many SO posts:

- (void)viewDidLoad{
    [self.spinner startAnimating];
    [SantiappsHelper fetchUsersWithCompletionHandler:^(NSArray *users) {
        self.usersArray = users;
        [self.tableView reloadData];
    }];
}

which will reload the tableview with the received data users after calling this method:

typedef void (^Handler)(NSArray *users);

+(void)fetchUsersWithCompletionHandler:(Handler)handler {
    NSURL *url = [NSURL URLWithString:@"http://www.somewebservice.com"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:10];
    [request setHTTPMethod: @"GET"];
    **// We dispatch a queue to the background to execute the synchronous NSURLRequest**
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        // Perform the request
        NSURLResponse *response;
        NSError *error = nil;
        NSData *receivedData = [NSURLConnection sendSynchronousRequest:request
                                                     returningResponse:&response
                                                                 error:&error];
        if (error) { **// If an error returns, log it, otherwise log the response**
            // Deal with your error
            if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
                NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
                NSLog(@"HTTP Error: %d %@", httpResponse.statusCode, error);
                return;
            }
            NSLog(@"Error %@", error);
            return;
        }
        **// So this line won't get processed until the response from the server is returned?**
        NSString *responseString = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];

        NSArray *usersArray = [[NSArray alloc] init];
        usersArray = [NSJSONSerialization JSONObjectWithData:[responseString dataUsingEncoding:NSASCIIStringEncoding] options:0 error:nil];
        // Finally when a response is received and this line is reached, handler refers to the block passed into this called method...so it dispatches back to the main queue and returns the usersArray
        if (handler){
            dispatch_sync(dispatch_get_main_queue(), ^{
            handler(usersArray);
            });
        }
    });
}

I can see it in the counter example, that the called method (with the passed block) will never exit the loop until it is done. Thus the 'completion' part actually depends on the code inside the called method, not the block passed into it?

In this case the 'completion' part depends on the fact that the call to NSURLRequest is synchronous. What if it was asynchronous? How would I be able to hold off calling the block until my data was populated by the NSURLResponse?

Stonedeaf answered 21/1, 2014 at 21:53 Comment(0)
R
3

Your first example is correct and complete and the best way to understand completion blocks. There is no further magic to them. They do not automatically get executed ever. They are executed when some piece of code calls them.

As you note, in the latter example, it is easy to call the completion block at the right time because everything is synchronous. If it were asynchronous, then you need to store the block in an instance variable, and call it when the asynchronous operation completed. It is up to you to arrange to be informed when the operation completes (possibly using its completion handler).

Do be careful when you store a block in an ivar. One of your examples includes:

   self.usersArray = users;

The call to self will cause the block to retain self (the calling object). This can easily create a retain loop. Typically, you need to take a weak reference to self like this:

- (void)viewDidLoad{
  [self.spinner startAnimating];
  __weak typeof(self) weakSelf = self;
  [SantiappsHelper fetchUsersWithCompletionHandler:^(NSArray *users) {
    typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
      [strongSelf setUsersArray:users];
      [[strongSelf tableView] reloadData];
    }
  }];
}

This is a fairly pedantic version of the weakSelf/strongSelf pattern, and it could be done a little simpler in this case, but it demonstrates all the pieces you might need. You take a weak reference to self so that you don't create a retain loop. Then, in the completely block, you take a strong reference so that self so that it can't vanish on you in the middle of your block. Then you make sure that self actually still exists, and only then proceed. (Since messaging nil is legal, you could have skipped the strongSelf step in this particular case, and it would be the same.)

Rivera answered 21/1, 2014 at 23:6 Comment(7)
Thanks @RobNapier, thats exactly the kind of answer I was looking for. Im trying to start building my own completion handlers. I was trying to explore the built in ones from UIKit like UIView animations but of course they are private :) One last thing, when you say, "If it were asynchronous, then you need to store the block in an instance variable, and call it when the asynchronous operation completed" how would I know when the asynchronous op is complete if that operation doesn't have a completion handler? Like processing images?Stonedeaf
Most asynchronous interfaces either have a completion block or a delegate method to indicate when they are complete. If it doesn't provide a way to notify you that it's done, it's not a very well designed async interface.Rivera
Ok, so then maybe Im thinking about how do you actually design a method that must be constantly checking for the results in order to provide a completion handler or callback to the user of that API. And I guess the answer is, it depends on what that method is waiting for and I would have to create the appropriate code to make sure something has been returned in order to provide a completion handler, huh? For example, I could be using an NSTimer that checks every 5 seconds to see if an NSURLResponse has been returned and then provide a completion handler? Like the loop example!Stonedeaf
You should always avoid polling if at all possible (and if it's not possible, there is probably something broken in the design). NSURLConnection provides delegate callbacks to let you know how it is progressing and when it has completed. You don't need to poll anything. All good async interfaces will have something like this.Rivera
No, I understand that. Im just thinking, if I were to build my own API that needed to check if a process is finished, I would need to add the completion part so that my users can benefit from a completion handler. Thus I would need to implement some code mechanism to constantly check for a value to be ready and then give my user a completion handler?Stonedeaf
Generally you would not need to poll this way. Almost anything you would be waiting for can be blocked on rather than polled for. For instance, you can use select() to wait on a port, or kqueues to wait on file modifications. You can use semaphores to wait for another operation to finish. If you find a situation where you think you have to poll, raise it on SO; it's very likely there's a way you can avoid polling. At the very bottom of the stack, inside the OS, there are some things that do poll, but you should very seldom need to at the application layer.Rivera
"Typically, you need to take a weak reference to self like this" In this example, there is no reference from self to the block. So I don't know what you are talking about regarding retain cycles. "so that it can't vanish on you in the middle of your block" That can't happen unless 1) something in that block indirectly removes the last reference to the block, which is unlikely, or 2) the last reference is removed from another thread, which would point to thread-safety problems.Improvised
T
2

Your first example (countToTenThousandAndReturnCompletionBLock) is actually a synchronous method. A completion handler doesn't make much sense here: Alternatively, you could call that block immediately after the hypothetical method countToTenThousand (which is basically the same, just without the completion handler).

Your second example fetchUsersWithCompletionHandler: is an asynchronous method. However, it's actually quite suboptimal:

  1. It should somehow signal the call-site that the request may have failed. That is, either provide an additional parameter to the completion handler, e.g. " NSError* error or us a single parameter id result. In the first case, either error or array is not nil, and in the second case, the single parameter result can be either an error object (is kind of NSError) or the actual result (is kind of NSArray).

  2. In case your request fails, you miss to signal the error to the call-site.

  3. There are code smells:

    As a matter of fact, the underlying network code implemented by the system is asynchronous. However, the utilized convenient class method sendSynchronousRequest: is synchronous. That means, as an implementation detail of sendSynchronousRequest:, the calling thread is blocked until after the result of the network response is available. And this_blocking_ occupies a whole thread just for waiting. Creating a thread is quite costly, and just for this purpose is a waste. This is the first code smell. Yes, just using the convenient class method sendSynchronousRequest: is by itself bad programming praxis!

    Then in your code, you make this synchronous request again asynchronous through dispatching it to a queue.

    So, you are better off using an asynchronous method (e.g. sendAsynchronous...) for the network request, which presumable signals the completion via a completion handler. This completion handler then may invoke your completion handler parameter, taking care of whether you got an actual result or an error.

Tractate answered 21/1, 2014 at 23:18 Comment(4)
Yes I know there is an asynchronous method to NSURLRequest. Im using synchronous here because thats precisely what im trying to understand, how to create a completion handler when I have to be notified somehow that data has finished downloading or images have finished processing.Stonedeaf
@Stonedeaf If you use an asynchronous network request, you basically put everything that needs to be "continued" after the response is available into the completion handler of that asynchronous network method.Tractate
Yes but if i put in a line to reload a view after the NSURLRequest asynchronous call, it will get called as soon as the NSURLRequest gets called, but there won't be an NSURLResponse yet.Stonedeaf
@Stonedeaf That's true, but irrelevant since you don't do this. Again, if you need to "continue" after the request has been finished, put that code into the completion handler of that request.Tractate

© 2022 - 2024 — McMap. All rights reserved.