Ive read quite bit about blocks by now, Apple's Guide, Cocoabuilder, 3 articles on SO and ive used examples in my code that I basically got from online tutorials. Im still trying to understand one specific question. So I decided to make an app with nothing more than a completionHandler example to understand better. This is what I came up with:
ViewController
- (void)viewDidLoad
[SantiappsHelper fetchUsersWithCompletionHandler:^(NSArray *users) {
self.usersArray = [NSMutableArray array];
for (NSDictionary *userDict in users) {
[self.usersArray addObject:[userDict objectForKey:#"username"]];
}
//WHILE TESTING postarray method, comment this out...
//[self getPoints];
[self.tableView reloadData];
}];
}
SantiappsHelper.h/m
typedef void (^Handler)(NSArray *users);
+(void)fetchUsersWithCompletionHandler:(Handler)handler {
NSString *urlString = [NSString stringWithFormat:#"http://www.myserver.com/myapp/getusers.php"];
NSURL *url = [NSURL URLWithString:urlString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:10];
[request setHTTPMethod: #"GET"];
__block NSArray *usersArray = [[NSArray alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
//dispatch_async(dispatch_get_main_queue(), ^{
// Peform the request
NSURLResponse *response;
NSError *error = nil;
NSData *receivedData = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
if (error) {
// Deal with your error
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
return;
}
NSLog(#"Error %#", error);
return;
}
NSString *responseString = [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding];
usersArray = [NSJSONSerialization JSONObjectWithData:[responseString dataUsingEncoding:NSASCIIStringEncoding] options:0 error:nil];
if (handler){
//dispatch_sync WAITS for the block to complete before returning the value
//otherwise, the array is returned but gets zeroed out
dispatch_sync(dispatch_get_main_queue(), ^{
handler(usersArray);
});
}
});
}
Here is what I understand...
I call fetchUsersWithCompletionHandler from my VC & pass it this completion block. That block takes an NSArray users parameter & returns void.
Meanwhile in the SantiappsHelper Class we have a variable called handler of type ^block which it receives from VC.
The fetchUsersWithCompletionHandler method runs, taking that CompletionBlock parameter, which itself takes the NSArray users parameter? a little confusing.
The webfetch is dispatch_async so it wont block the main thread. So execution on the main thread continues. That new thread executes the fetch synchronously that new thread will stall until the response is returned. Once that new thread receives the response, it fills in the NSHTTPURLResponse. Once it returns, it fills in usersArray with the NSJSONSerialized data.
Finally it reaches the if test and it checks for the existence of the PARAMETER handler that was passed in? A little confusing...the parameter passed in was the completionBlock. Wouldnt that handler parameter always and obviously exist since it was passed in?
Once the handler !NULL then execution is returned to the main thread passing back the NSArray users expected by the block back in the VC.
But if I change the second dispatch to async, the usersArray is properly populated but once handler(usersArray) is sent back to the main thread, its empty or nil! Why?
Correct. The best way to say/think about this is to say that you are invoking a method called fetchUsersWithCompletionHandler:. This method will go away and do some work and at some point in the future it may execute the code you have declared in the block literal and pass in an array of users.
The method takes an argument called handler of type void (^)(NSArray *users). This type represents a block of code that when invoked should receive and array and return no result.
The fetchUsersWithCompletionHandler: does some work and at some point may invoke the block passed in with an array of users as the blocks argument.
Correct
The if (handler) { checks to see if the handler arguments is not nil. In most cases this would be the case especially if you always invoke the fetchUsersWithCompletionHandler: with a block literal, but you could always invoke the method with [self fetchUsersWithCompletionHandler:nil]; or invoked it passing along a variable from somewhere else as the completion, which may be nil. If you try to dereference nil to invoke it then you will crash.
Execution is not "passed back" to the main thread, you are simply enqueueing a block of work to be performed on the main thread. You are doing this with dispatch_sync call which will block this background thread until the block completes - this isn't really required.
The array being nil could be a consequence of you declaring the usersArray with __block storage. This is not required as you are not modifying what usersArray is pointing to at any point you are simply calling methods on it.
Related
I have a class with a single method, that uses a URLConnection to send a serialized NSDictionary to a script at a certain URL, and then calls a completion block. Here is the code for that method:
- (void)sendDictionary:(NSDictionary *)dictionary toScript:(NSString *)scriptName completion:(void (^) (id response))completionBlock
{
...Serialize data and add it to an NSURLRequest request...
H2URLConnection *connection = [[H2URLConnection alloc]initWithRequest:request];
//Define a semaphore to block execution of later statements until the signal is received.
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
[connection setCompletionBlock:[^(id obj, NSError *err)
{
if (!err) {
//Catch the server response
NSString *receivedString = [[NSString alloc] initWithData:obj encoding:NSUTF8StringEncoding];
NSLog( #"ChecklistAppNetworkManager received string: %#", receivedString);
//Convert the JSON response into an NSDictionary
NSError *otherError;
id deserializedJSON = [NSJSONSerialization JSONObjectWithData:obj options:kNilOptions error:&otherError];
if (otherError) {
NSLog(#"ChecklistAppNetworkManager JSON Error: %#", otherError.description);
}
[completionBlock invoke];
NSLog(#"ChecklistAppNetworkManager JSON Response: %#", deserializedJSON);
//Dispatch the semaphore signal so that the main thread continues.
dispatch_semaphore_signal(sem);
} else {
NSLog(#"ChecklistAppNetworkManager encountered an error connecting to the server: %#", [err description]);
}
}copy]];
//Finalize and initate the connection.
[connection start];
//Since block is dispatched to main queue, stall with a loop until the semaphore signal arrives.
while (dispatch_semaphore_wait(sem, DISPATCH_TIME_NOW)) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}
}
I'm trying to call this method on an instance of this class from within another class, where the completion block is defined. Here's the code where I get the EXC_BAD_ACCESS:
- (void)doSomeServerTask
{
H2ChecklistAppNetworkManager *currentNetworkManager = ((H2AppDelegate *)[[UIApplication sharedApplication]delegate]).networkManager; //Instantiate class where that method is defined
NSMutableDictionary *dictonary = [NSMutableDictionary dictionary];
...populate dictionary...
[currentNetworkManager sendDictionary:dictionary toScript:#"script.php" completion:[^(id response)
{ //THIS iS THE LINE WHERE THE BAD ACCESS OCCURS
NSLog(#"LoginViewController received response: %#", response);
} copy]];
}
Any help would be appreciated!
The completionBlock on that method takes one argument, but you call the block with the invoke method. More likely than not, the crash is because the runtime is trying to retain whatever garbage is in memory that should be that argument.
However, you really need to refactor this code entirely. Blocking the main event loop is bad. Running a sub-runloop is even worse on the MEL; it changes the way dispatch queue handling semantics work and can lead to pathologically bad performance or behavior.
You should move to a truly asynchronous model. If the app can't proceed until these queries are done, then put up a modal indicator that blocks progress.
To do that, you structure the code loosely as:
• put user interface into a "loading..." or some other modal state
• execute an asynchronous request for data with a completion handler
• in the completion handler, dispatch the "update UI" request to the main queue
• upon "update UI", tear down your modal "loading...." UI and update the display for the user
There is no need to block the main event loop to do any of this.
I think I might have an async problem going on here, which bites cause I thought I had solved it. Anyway, I am making a bunch of web service calls like so:
//get the client data
__block NSArray* arrClientPAs;
[dataManager getJSONData:strWebService withBlock:^(id results, NSError* error) {
if (error) {
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:#"Getting Client Data Error!" message:error.description delegate:nil cancelButtonTitle:NSLocalizedString(#"Okay", nil) otherButtonTitles:nil, nil];
[alert show];
} else {
arrClientPAs = results;
}
}];
and getJSONData is like so:
- (void) getJSONData : (NSString*) strQuery withBlock:(void (^)(id, NSError *))completion {
NSDictionary* dictNetworkStatus = [networkManager checkNetworkConnectivity];
NetworkStatus networkStatus = [[dictNetworkStatus objectForKey:#"Status"] intValue];
if (networkStatus != NotReachable) {
//set up the url for webservice
NSURL* url = [NSURL URLWithString:strQuery];
NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:url];
//set up the url connection
__block id results;
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue mainQueue] completionHandler:
^(NSURLResponse* response, NSData* jsonData, NSError* error) {
if (error) {
completion(nil, error);
return;
}
results = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves | NSJSONReadingAllowFragments error:&error];
completion(results, nil);
}];
} else {
//not connected to a network - data is going to have to come from coredata
}
}
In the first block, if I log arrClientData I can see the data that I am expecting but when I log arrClientData after it it is nil. I was following this SO thread - How to return a BOOL with asynchronous request in a method? (Objective-C) and a couple of others.
Obviously I am trying to get the data after the async call is made. Any help would be appreciated.
The problem lies, I think, in what "asynchronous" means. Here's a diagram:
Step One
__block result;
Step Two - do something asynchonous, including e.g. setting result
Step Three
What order do things happen in here? Step Three happens before Step Two gets finished. That is what asynchronous means: it means, "go right on with this code, don't wait for the asynchronous stuff to finish." So at the time Step Three happens, the result variable has not yet been set to anything.
So, you are just misleading the heck out of yourself with your __block result. __block or no __block, there is no way you are going to find out out what the result is afterwards, because there is no "afterwards". Your code has completed before your __block result is even set. That is why asynchronous code uses a callback (eg. your completion block) which does run afterwards, because it is sequentially part of (appended to) the asynchronous code. You can hand your result downwards through the callback, but you cannot usefully set it upwards from within the block and expect to retrieve it later.
So, your overall structure is like this:
__block NSArray* arrClientPAs; // it's nil
[call getJSONdata] = step one
[call sendAsynchronousRequest]
do the block _asynchronously_ = step two, tries to set arrClientPAs somehow
step three! This happens _before_ step two, ...
... and this entire method ends and is torn down ...
... and arrClientPAs is still nil! 🌻
I repeat: you cannot pass any information UP out of an asynchronous block. You can only go DOWN. You need your asynchronous block to call some method of some independently persistent object to hand it your result and tell it to use that result (and do it carefully, on the main thread, or you will cause havoc). You cannot use any automatic variable for this purpose, such as your declared NSArray variable arrClientPAs; there is no automatic scope any more, the method is over, the automatic variable is gone, there is no more code to run.
Check the value of the 'error 'variable after call:
results = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves | NSJSONReadingAllowFragments error:&error];
If 'error' isn't nil there is a problem with data which you get in your completion block.
You are mixing styles and confusing the purpose of __block.
Note: When you call a method that will be executed asynchronously you are creating a new execution path which will be executed at some point in the future (which includes immediately) on some thread.
In your getJSONData method you use a __block qualified variable, results, when you should not. The variable is only required within the block and should be declared there:
//set up the url connection
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue mainQueue] completionHandler:
^(NSURLResponse* response, NSData* jsonData, NSError* error)
{
if (error) {
completion(nil, error);
return;
}
id results = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves | NSJSONReadingAllowFragments error:&error];
completion(results, nil);
}];
Declaring the variable outside of the block and adding __block just adds pointless complexity. After the call to sendAsynchronousRequest, returns before the request has been performed, the value of results would not be the value assigned in the block. The call to the completion block is performed on a different execution path and probably will not even be executed until after the call to getJSONData has returned.
However what is correct about your getJSONData method is its model - it takes a completion block which sendAsynchronousRequest's own completion handler will call. This is what is incorrect about your call to getJSONData - the completion block you pass does not pass on the results to another block or pass them to some object, but instead assigns them a local variable, arrClientPAs, declared before the call. This is the same situation as described above for getJSONData and will fail for the same reasons - it is not the arrClientPAs fails to "retain the data" but that you are reading it on in the current execution path before another execution path has written any data to it.
You can address this problem the same way getJSONData does - the enclosing method (not included in your question) can take a completion block (code entered directly into answer, expect typos!):
- (void) getTheClientData: ... completionHandler:(void (^)(id))handler
{
...
//get the client data
[dataManager getJSONData:strWebService withBlock:^(id results, NSError* error) {
if (error) {
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:#"Getting Client Data Error!" message:error.description delegate:nil cancelButtonTitle:NSLocalizedString(#"Okay", nil) otherButtonTitles:nil, nil];
[alert show];
} else {
handler(results); // "return" the result to the handler
}
}];
There is another approach. If and only if getClientData is not executing on the main thread and you wish its behaviour to be synchronous and to return the result of the request then you can issue a sendSynchronousRequest:returningResponse:error: instead of an asynchronous one. This will block the thread getClientData is executing on until the request completes.
In general if you have an asynchronous method which you cannot replace by a synchronous one but require synchronous behaviour you can use semaphores to block your current thread until the asynchronous call completes. For an example of how to do this see this answer.
HTH
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?
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.)
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:
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).
In case your request fails, you miss to signal the error to the call-site.
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.
I'm attempting to appropriately use NSURLConnection's sendAsynchronousRequest method but I'm having trouble with the block parameter. I built the block above the call and am using __block variables to attempt to retrieve the data collected within it. I've confirmed that the data within the block is correct so the block is working just fine, leaving the retrieval of the block data the problem. My assignments and return statement at the end of the method are not being handed the correct values and I assume it is because the the block has yet to finish executing before the assignment is taking place. How can I "wait" until the block has finished executing before performing the assignments without interrupting the main thread?
- (NSData *) sendAsynchronousURLRequest: (NSMutableURLRequest *)request withResponse: (NSURLResponse *)response andError:(NSError *)error
{
// Prepare the block handler.
__block NSURLResponse *retrievedBlockResponse;
__block NSError *retrievedBlockError;
__block NSData *retrivedBlockData;
void(^requestHandler)(NSURLResponse*, NSData*, NSError*) =
^(NSURLResponse *blockResponse, NSData *blockData, NSError *blockError)
{
retrievedBlockResponse = blockResponse;
retrivedBlockData = blockData;
retrievedBlockError = blockError;
// NOTE: Correct data is here...
NSLog(#"Data Inside Block:\n%#", [NSMutableDictionary DictionaryFromJSONData:blockData errorReport:nil]);
};
// Send the asynchronous request (operationQueue initialized elsewhere in class).
[NSURLConnection sendAsynchronousRequest:request queue:operationQueue completionHandler:requestHandler];
// NOTE: Reference to data cannot be established here!
// Establish references to block variables and return NSData object.
response = retrievedBlockResponse;
error = retrievedBlockError;
return retrivedBlockData;
}
Using iOS 5's new TWRequest API, I've ran into a brick wall related with block usage.
What I need to do is upon receiving a successful response to a first request, immediately fire another one. On the completion block of the second request, I then notify success or failure of the multi-step operation.
Here's roughly what I'm doing:
- (void)doRequests
{
TWRequest* firstRequest = [self createFirstRequest];
[firstRequest performRequestWithHandler:^(NSData* responseData,
NSHTTPURLResponse* response,
NSError* error) {
// Error handling hidden for the sake of brevity...
TWRequest* secondRequest = [self createSecondRequest];
[secondRequest performRequestWithHandler:^(NSData* a,
NSHTTPURLResponse* b,
NSError* c) {
// Notify of success or failure - never reaches this far
}];
}];
}
I am not retaining either of the requests or keeping a reference to them anywhere; it's just fire-and-forget.
However, when I run the app, it crashes with EXC_BAD_ACCESS on:
[secondRequest performRequestWithHandler:...];
It executes the first request just fine, but when I try to launch a second one with a handler, it crashes. What's wrong with that code?
The methods to create the requests are as simple as:
- (TWRequest*)createFirstRequest
{
NSString* target = #"https://api.twitter.com/1/statuses/home_timeline.json";
NSURL* url = [NSURL URLWithString:target];
TWRequest* request = [[TWRequest alloc]
initWithURL:url parameters:params
requestMethod:TWRequestMethodGET];
// _twitterAccount is the backing ivar for property 'twitterAccount',
// a strong & nonatomic property of type ACAccount*
request.account = _twitterAccount;
return request;
}
Make sure you're keeping a reference/retaining the ACAccountStore that owns the ACAccount you are using to sign the TWRequests.
If you don't, the ACAccount will become invalid and then you'll get EXC_BAD_ACCESS when trying to fire a TWRequest signed with it.
I'm not familiar with TW*, so consider this a wild guess ... try sending a heap-allocated block:
[firstRequest performRequestWithHandler:[^ (NSData *responseData, ...) {
...
} copy]];
To clarify, I think the block you're sending is heap-allocated, so while TW* might be retaining it, it won't make any difference if it has already gone out of scope.