when i use sendAsynchronousRequest method at that time this method inner block will execute after outside code will execute?
__block NSDictionary *dict;
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
NSInteger httpStatus = [((NSHTTPURLResponse *)response) statusCode];
NSLog(#"httpStatus inside block:%d",httpStatus);
if ([data length]>0 && connectionError==nil)
{
dict=[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers|NSJSONReadingAllowFragments error:nil];
}
else if (connectionError)
{
UIAlertView *alt=[[UIAlertView alloc] initWithTitle:#"Error" message:[connectionError localizedDescription] delegate:self cancelButtonTitle:#"Ok" otherButtonTitles:nil, nil];
[alt show];
}
}];
return dict;//it will return null because it will run before execute inner block of sendAsynchronousRequest
Asynchronous request always run in the network thread.
Your [NSOperationQueue mainQueue] means that the block-callback will schedule in mainQueue.
If you wanna to wait until the request finished, you can use synchronous request, or use semaphore to block current thread.
See
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error;
but attention that this way to request will block current thread. be careful when using this way.
You should use block method
First define a block
typedef void (^OnComplate) (NSDictionary * dict);
- (void)viewDidLoad{
[self getDataWithBlokc:^(NSDictionary *dict) {
NSLog(#"%#",dict);
}];
}
Use the following method
-(void)getDataWithBlokc:(OnComplate)block{
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
NSInteger httpStatus = [((NSHTTPURLResponse *)response) statusCode];
NSLog(#"httpStatus inside block:%d",httpStatus);
if ([data length]>0 && connectionError==nil)
{
NSDictionary* dict=[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers|NSJSONReadingAllowFragments error:nil];
block(dict);
}
else if (connectionError)
{
UIAlertView *alt=[[UIAlertView alloc] initWithTitle:#"Error" message:[connectionError localizedDescription] delegate:self cancelButtonTitle:#"Ok" otherButtonTitles:nil, nil];
[alt show];
}
}];
}
Related
I've created a class called "ConnectionManager" that will handle all network request and fetch data from the server and after that call to a completion handler.
ConnectionManager.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "UIAlertView+CustomAlertView.h"
#import <Crashlytics/Crashlytics.h>
#interface ConnectionManager : NSObject<NSURLSessionDataDelegate>
#property (weak, nonatomic) NSMutableData *receivedData;
#property (weak, nonatomic) NSURL *url;
#property (weak, nonatomic) NSURLRequest *uploadRequest;
#property (nonatomic, copy) void (^onCompletion)(NSData *data);
#property BOOL log;
-(void)downloadDataFromURL:(NSURL *)url withCompletionHandler:(void (^)(NSData *data))completionHandler;
-(void)uploadDataWithRequest:(NSURLRequest*)request withCompletionHandler:(void (^)(NSData *data))completionHandler;
#end
ConnectionManager.m
#import "ConnectionManager.h"
#implementation ConnectionManager
-(void)uploadDataWithRequest:(NSURLRequest*)request withCompletionHandler:(void (^)(NSData *data))completionHandler{
// Instantiate a session configuration object.
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
// Configure Session Configuration
[configuration setAllowsCellularAccess:YES];
// Instantiate a session object.
NSURLSession *session=[NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
// Assign request for later call
self.uploadRequest = request;
// Create an upload task object to perform the data uploading.
NSURLSessionUploadTask *task = [session uploadTaskWithRequest:self.uploadRequest fromData:nil];
// Assign completion handler
self.onCompletion = completionHandler;
// Inititate data
self.receivedData = [[NSMutableData alloc] init];
// Resume the task.
[task resume];
}
-(void)downloadDataFromURL:(NSURL *)url withCompletionHandler:(void (^)(NSData *data))completionHandler{
// Instantiate a session configuration object.
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
// Configure Session Configuration
[configuration setAllowsCellularAccess:YES];
// Instantiate a session object.
NSURLSession *session=[NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
// Assign url for later call
self.url = url;
// Create a data task object to perform the data downloading.
NSURLSessionDataTask *task = [session dataTaskWithURL:self.url];
// Assign completion handler
self.onCompletion = completionHandler;
// Inititate data
self.receivedData = [[NSMutableData alloc] init];
// Resume the task.
[task resume];
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[self.receivedData appendData:data];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
if (error.code == -1003 || error.code == -1009) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:#"Unable to connect to the server. Please check your internet connection and try again!" delegate:nil cancelButtonTitle:#"cancel" otherButtonTitles:#"retry",nil];
[alert showWithCompletion:^(UIAlertView *alertView, NSInteger buttonIndex) {
if (buttonIndex==1) {
// Retry
if (self.url) {
NSURLSessionDataTask *retryTask = [session dataTaskWithURL:self.url];
[retryTask resume];
}else{
NSURLSessionUploadTask *task = [session uploadTaskWithRequest:self.uploadRequest fromData:nil];
[task resume];
}
}else{
self.onCompletion(nil);
}
}];
}];
}else{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:#"An unkown error occured! Please try again later, thanks for your patience." delegate:nil cancelButtonTitle:#"cancel" otherButtonTitles:#"retry",nil];
[alert show];
CLS_LOG(#"Error details: %#",error);
self.onCompletion(nil);
}];
}
}
else {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.onCompletion(self.receivedData);
}];
}
}
#end
Here is the piece of code I use to call it :
-(void)loadDataFromServer{
NSString *URLString = [NSString stringWithFormat:#"%#get_people_number?access_token=%#", global.baseURL, global.accessToken];
URLString = [URLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:URLString];
ConnectionManager *connectionManager = [[ConnectionManager alloc] init];
[connectionManager downloadDataFromURL:url withCompletionHandler:^(NSData *data) {
if (data != nil) {
// Convert the returned data into an array.
NSError *error;
NSDictionary *number = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
if (error != nil) {
CLS_LOG(#"Error: %#, Response:%#",error,[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
}
else{
[_mapView updateUIWithData:[number objectForKey:#"number"]];
}
}
}];
}
I figured out when using Instruments that All objects of ConnectionManager type are persistent even after getting the data from server and calling the completion handler.
I tried to change the completion handler property from copy to strong, but I got the same results. Changing it to weak cause a crash and it never be called.
Please someone guide me to the right way.
After doing a lot of research, I found that the URL Session object is still alive.
Based on Apple Documentation Life Cycle of URL Session there's two cases:
Using the system provided delegate (by not setting a delegate), here the system will take care of invalidating the NSURLSession object.
Using a custom delegate. When you no longer need a session, invalidate it by calling either invalidateAndCancel (to cancel outstanding tasks) or finishTasksAndInvalidate (to allow outstanding tasks to finish before invalidating the object).
To fix the code above I only changed the delegate method "didCompleteWithError" like the following:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
if (error.code == -1003 || error.code == -1009) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:#"Unable to connect to the server. Please check your internet connection and try again!" delegate:nil cancelButtonTitle:#"cancel" otherButtonTitles:#"retry",nil];
[alert showWithCompletion:^(UIAlertView *alertView, NSInteger buttonIndex) {
if (buttonIndex==1) {
// Retry
if (self.url) {
NSURLSessionDataTask *retryTask = [session dataTaskWithURL:self.url];
[retryTask resume];
}else{
NSURLSessionUploadTask *task = [session uploadTaskWithRequest:self.uploadRequest fromData:nil];
[task resume];
}
}else{
[session finishTasksAndInvalidate];
self.onCompletion(nil);
}
}];
}];
}else{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:#"An unkown error occured! Please try again later, thanks for your patience." delegate:nil cancelButtonTitle:#"cancel" otherButtonTitles:#"retry",nil];
[alert show];
[session finishTasksAndInvalidate];
self.onCompletion(nil);
}];
}
}
else {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[session finishTasksAndInvalidate];
self.onCompletion(self.receivedData);
}];
}
}
I am developing a login application. I have a registration view, I'm using sendAsynchronousRequest to send registration request.When I received a data back I want to show alert view displaying the registration if valid. Now my problem is When I received a data back the UI get blocked until alert view display.
why is that happen and how could I fix it ?
check the the code snippet:
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
error = connectionError;
NSMutableDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil ];
if ([dic count] == 1) {
[dic valueForKey:#"RegisterUserResult"];
UIAlertView *alertview = [[UIAlertView alloc] initWithTitle:#"Registration" message:[dic valueForKey:#"RegisterUserResult"] delegate:self cancelButtonTitle:#"cancel" otherButtonTitles:nil];
[alertview show];
}else {
UIAlertView *alertview = [[UIAlertView alloc] initWithTitle:#"Registration" message:[dic valueForKey:#"ErrorMessage"] delegate:self cancelButtonTitle:#"cancel" otherButtonTitles:nil];
[alertview show];
}
[self printData:data];
You try to perform UI operations on non-main thread because completion block executes on you custom operation queue.
You need to wrap all UI calls in
dispatch_async(dispatch_get_main_queue(), ^{
......
});
within custom operation queue non-main threads.
So, in your code any call for UIAlertView will looks like
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *alertview = [[UIAlertView alloc] initWithTitle:#"Registration" message:[dic valueForKey:#"RegisterUserResult"] delegate:self cancelButtonTitle:#"cancel" otherButtonTitles:nil];
[alertview show];
});
If you have no heavy operations in your completion handler you can dispatch it on main thread directly setting queue to [NSOperationQueue mainQueue]
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {...}];
So just like the question suggests, I'm trying not to freeze up the UI after the user sends some data to the server. In my case, they're possibly sending a lot of data and server side I have to do a foreach loop for multiple queries.
While all that is happening, I don't want the user to wait for a response so I'm dismissing the modal VC after "Send" is clicked. The data still gets inserted into the database but what if there's an error? Right now I'm showing a UIAlertView after the modal VC is dismissed but I get a bad access error. What's the best way of showing an error?
- (void)send:(id)sender{
if ([[Data sharedInstance].someData objectForKey:#"time"] != NULL) {
[self dismissViewControllerAnimated:YES completion:^(){
NSMutableDictionary *paramDic = [NSMutableDictionary new];
[paramDic setValue:[[Data sharedInstance].someData objectForKey:#"oneArray"] forKeyPath:#"oneArray"];
[paramDic setValue:[NSArray arrayWithObjects:[[[Data sharedInstance].someData objectForKey:#"two"] valueForKey:#"Name"], [[[Data sharedInstance].someData objectForKey:#"two"] valueForKey:#"State"], [[[Data sharedInstance].someData objectForKey:#"two"] valueForKey:#"Country"], nil] forKeyPath:#"twoArray"];
[paramDic setValue:[[[Data sharedInstance].someData objectForKey:#"three"] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding] forKeyPath:#"three"];
[paramDic setValue:[[NSUserDefaults standardUserDefaults] valueForKey:#"username"] forKey:#"username"];
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:paramDic options:NSJSONWritingPrettyPrinted error:nil];
NSURL *url = [NSURL URLWithString:#"http://localhost/myapp/handleData.php"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"POST"];
[request setValue:#"application/json" forHTTPHeaderField:#"Content-Type"];
NSString *length = [NSString stringWithFormat:#"%d", jsonData.length];
[request setValue:length forHTTPHeaderField:#"Content-Length"];
[request setValue:#"json" forHTTPHeaderField:#"Data-Type"];
[request setHTTPBody:jsonData];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if (![httpResponse statusCode] == 200 || ![[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding] isEqualToString:#"success"]) {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"Error" message:#"Problem on the server. Please try again later." delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[alert show];
}
}];
}];
}
That's how I'm doing it now...what's a better way?
The above answer is valuable. But I think this is what causes the problem.
Change:
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"Error" message:#"Problem on the server. Please try again later." delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
To:
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"Error" message:#"Problem on the server. Please try again later." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
Hope this helps.
I'm thinking the issue has to do with your nested completion blocks. Because you place your web service call in the modal dismissal completion block, by the time it finishes and starts to execute your URL completion block, the class/controller in charge of that block has already been forgotten/destroyed/deallocated/whatever. At the very least, the view no longer exists where you're trying to present your alert view. You've listed self as the delegate of that alert view, but self doesn't exist anymore.
Try this first: instead of trying to display your alert on a no-longer-in-memory view from a background thread (big no-no even if the view still existed), try posting a notification to the main thread that you can pick up elsewhere in your app (such as your root view controller) and present the alert from there:
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if (![httpResponse statusCode] == 200 || ![[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding] isEqualToString:#"success"]) {
dispatch_async(dispatch_get_main_queue(),^{
// observe this notification in root view controller or somewhere
// that you know will be in memory when it fires
[[NSNotificationCenter defaultCenter] postNotificationName:#"ErrorAlert"
object:nil
userInfo:imageDict];
});
}
}];
If that still doesn't work because the app has discarded your completion block when the view was dismissed, you will need to create a separate class that is in charge of sending this web service request instead of doing it all directly in the completion block. But still, remember to present your alert on the main thread :)
This code loads a table view:
- (void)viewDidLoad
{
[super viewDidLoad];
//test data
NSURL *url =[[NSURL alloc] initWithString:urlString];
// NSLog(#"String to request: %#",url);
[ NSURLConnection
sendAsynchronousRequest:[[NSURLRequest alloc]initWithURL:url]
queue:[[NSOperationQueue alloc]init]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if([data length] >0 && connectionError ==nil){
NSArray *arrTitle=[[NSArray alloc]init];
NSString *str=[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
arrTitle= [Helper doSplitChar:[Helper splitChar20] :str];
self.tableView.delegate = self;
self.tableView.dataSource = self;
[self fecthDataToItem:arrTitle];
[self.tableView reloadData];
NSLog(#"Load data success");
}else if (connectionError!=nil){
NSLog(#"Error: %#",connectionError);
}
}];
// arrTitle = [NSArray arrayWithObjects:#"ee",#"bb",#"dd", nil];
}
And it takes 10 - 15s to load. How can I make this faster?
.
Thanks Rob and rmaddy, problem is solve.
As rmaddy points out, you must do UI updates on the main queue. Failure to do so will, amongst other things, account for some of the problems you're experiencing.
The queue parameter of sendAsynchronousRequest indicates the queue upon which you want the completion block to run. So, you can simply specify [NSOperationQueue mainQueue]:
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if([data length] > 0 && connectionError == nil) {
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSArray *arrTitle = [Helper doSplitChar:[Helper splitChar20] :str];
self.tableView.delegate = self;
self.tableView.dataSource = self;
[self fecthDataToItem:arrTitle];
[self.tableView reloadData];
} else if (connectionError!=nil) {
NSLog(#"Error: %#",connectionError);
}
}];
Or, if you where doing something slow or computationally expensive/slow within that block, go ahead and use your own background queue, but then dispatch the UI updates back to the main queue, e.g.:
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// do something computationally expensive here
// when ready to update the UI, dispatch that back to the main queue
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// update your UI here
}];
}];
Either way, you should always do UI updates (and probably model updates, too, to keep that synchronized) on the main queue.
I have a block to use as a completionHandler for an NSURLConnection asynchronous request whose main job is to spawn a new asynchronous request using the same block for the new requests completion handler. I am doing this because it effectively solves another problem which is to line up a sequence of asynchronous calls and have them fired off in the background. This is working marvelously for us, but we have a warning I am concerned about. Namely, XCode thinks I have a retain cycle. Perhaps I do, I don't know. I've been trying to learn about blocks over the last couple hours but I haven't found an explanation for recursive uses like mine. The warning states `Block will be retained by the captured object'.
My best guess so far is that a retain cycle is exactly what we want, and that to clear when we are done, we just nillify the block variable, which I'm doing. It doesn't get rid of the error, but I don't mind as long as I'm not leaking memory or doing some black magic I'm not aware of. Can anyone address this? Am I handling it right? If not, what should I be doing?
void (^ __block handler)(NSURLResponse *, NSData *, NSError*);
handler = ^(NSURLResponse *response, NSData *data, NSError *error)
{
[dataArray addObject:data];
if (++currentRequestIndex < [requestsArray count])
{
if (error)
{
[delegate requestsProcessWithIdentifier:_identifier processStoppedOnRequestNumber:currentRequestIndex-1 withError:error];
return;
}
[delegate requestsProcessWithIdentifier:_identifier completedRequestNumber:currentRequestIndex-1]; // completed previous request
[NSURLConnection sendAsynchronousRequest:[requestsArray objectAtIndex:currentRequestIndex]
queue:[NSOperationQueue mainQueue]
completionHandler:handler]; // HERE IS THE WARNING
}
else
{
[delegate requestsProcessWithIdentifier:_identifier completedWithData:dataArray];
handler = nil;
}
};
[NSURLConnection sendAsynchronousRequest:[requestsArray objectAtIndex:0]
queue:[NSOperationQueue mainQueue]
completionHandler:handler];
Try to store your handler block into an instance variable of your view controller (or whatever class you're in).
Assuming that you declare an instance variable named _hander:
{
void (^_handler)(NSURLResponse *, NSData *, NSError*);
}
Change your code to:
__weak __typeof(&*self)weakSelf = self;
_handler = ^(NSURLResponse *response, NSData *data, NSError *error)
{
[dataArray addObject:data];
if (++currentRequestIndex < [requestsArray count])
{
if (error)
{
[delegate requestsProcessWithIdentifier:_identifier processStoppedOnRequestNumber:currentRequestIndex-1 withError:error];
return;
}
[delegate requestsProcessWithIdentifier:_identifier completedRequestNumber:currentRequestIndex-1]; // completed previous request
__strong __typeof(&*self)strongSelf = weakSelf;
[NSURLConnection sendAsynchronousRequest:[requestsArray objectAtIndex:currentRequestIndex]
queue:[NSOperationQueue mainQueue]
completionHandler:strongSelf->_handler];
}
else
{
[delegate requestsProcessWithIdentifier:_identifier completedWithData:dataArray];
}
};
[NSURLConnection sendAsynchronousRequest:[requestsArray objectAtIndex:0]
queue:[NSOperationQueue mainQueue]
completionHandler:_handler];
void (^handler)(NSURLResponse *, NSData *, NSError*);
typeof(handler) __block __weak weakHandler;
weakHandler = handler = ^(NSURLResponse *response, NSData *data, NSError *error)
{
[dataArray addObject:data];
if (++currentRequestIndex < [requestsArray count])
{
if (error)
{
[delegate requestsProcessWithIdentifier:_identifier processStoppedOnRequestNumber:currentRequestIndex-1 withError:error];
return;
}
[delegate requestsProcessWithIdentifier:_identifier completedRequestNumber:currentRequestIndex-1]; // completed previous request
[NSURLConnection sendAsynchronousRequest:[requestsArray objectAtIndex:currentRequestIndex]
queue:[NSOperationQueue mainQueue]
completionHandler:weakHandler]; // HERE IS THE WARNING
}
else
{
[delegate requestsProcessWithIdentifier:_identifier completedWithData:dataArray];
}
};