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];
}
};
Related
I have an iOS app with a function which is in charge of making an asynchronous network request. The request itself works just fine, but the problem I am having is with the function return statement which is causing errors.
Here is my function:
-(NSArray *)get_data:(NSString *)size {
// Set up the data request.
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"http://mywebsite.com/info.json"]];
NSURLRequest *url_request = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// Begin the asynchronous data loading.
[NSURLConnection sendAsynchronousRequest:url_request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (error == nil) {
// Convert the response JSON data to a dictionary object.
NSError *my_error = nil;
NSDictionary *feed = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:&my_error];
if (feed != nil) {
// Store the returned data in the data array.
NSArray *topping_data;
for (int loop = 0; loop < [[feed objectForKey:#"toppings_data"] count]; loop++) {
NSString *size_name = [NSString stringWithFormat:#"%#", [[[feed objectForKey:#"toppings_data"] objectAtIndex:loop] valueForKey:#"Size"]];
if ([size_name isEqualToString:size]) {
topping_data = [[feed objectForKey:#"toppings_data"] objectAtIndex:loop];
}
}
return topping_data;
}
else {
return #[#"no data"];
}
}
else {
return #[#"no data"];
}
}];
}
I am getting the following error message on the line of code [NSURLConnection sendAsync....:
Incompatible block pointer types sending 'NSArray *(^)(NSURLResponse
*__strong, NSData *__strong, NSError *__strong)' to parameter of type 'void (^ _Nonnull)(NSURLResponse * _Nullable __strong, NSData *
_Nullable __strong, NSError * _Nullable __strong)'
What am I doing wrong here?
All I am trying to avoid, is the function returning BEFORE the asynchronous request has completed. Otherwise the function will not return any data, which is not what I want.
Thanks for your time, Dan.
best way to return data in async block is make a block callback as argument of function and callback return value here:
- (void)get_data:(NSString *)size completionHandler:(void (^)(NSArray *array))completionHandler {
// ...
[NSURLConnection sendAsynchronousRequest:url_request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
// ...
completionHandler(array);
// ...
}];
}
use :
[self get_data:someString completionHandler:^(NSArray *array) {
// process array here
}];
The block returns nothing:
void ^(NSURLResponse *, NSData *, NSError *)
So you cannot return things:
return #[#"no data"];
The code that calls the block is not interested in what it returns; if you want to store state then add an instance variable or call a method.
Change
[NSURLConnection sendAsynchronousRequest:url_request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
to
[NSURLConnection sendAsynchronousRequest:url_request queue:queue completionHandler:^(NSURLResponse *_Nullable response, NSData *_Nullable data, NSError *_Nullable error)
Basically I want a way to issue a NSURLRequest multiple times in a loop until a certain condition has been met. I am using a rest api but the rest api only allows up to a maximum of 1,000 results at a time. So if i have, lets say 1,500 total, i want to make a request to get the first 1,000 then i need to get the rest with another almost exact request , except the startAt: parameter is different(so i could go from 1001 - 1500. I want to set this up in a while loop(while i am done loading all the data) and am just reading about semaphores but its not working out like I expected it to. I don't know how many results I have until i make the first request. It could be 50, 1000, or 10,000.
here is the code:
while(!finishedLoadingAllData){
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLRequest *myRequest = [self loadData: startAt:startAt maxResults:maxResults];
[NSURLConnection sendAsynchronousRequest:myRequest
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if(error){
completionHandler(issuesWithProjectData, error);
}
else{
NSDictionary *issuesDictionary = [[NSDictionary alloc] initWithDictionary:[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]];
[issuesWithProjectData addObjectsFromArray:issuesDictionary[#"issues"]];
if(issuesWithProjectData.count == [issuesDictionary[#"total"] integerValue]){
completionHandler([issuesWithProjectData copy], error);
finishedLoadingAllData = YES;
}
else{
startAt = maxResults + 1;
maxResults = maxResults + 1000;
}
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
Basically I want to keep the while loop waiting until the completion block finished. Then and only then do i want the while loop to check if we have all of the data or not(and if not, make another request with the updated startAt value/maxResults value.
Right now it just hangs on dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
What am i doing wrong or what do i need to do? Maybe semaphores are the wrong solution. thanks.
Ok. The more I look, the more I don't think its a bad idea to have semaphores to solve this problem, since the other way would be to have a serial queue, etc. and this solution isn't all that more complicated.
The problem is, you are requesting the completion handler to be run on the main thread
[NSURLConnection sendAsynchronousRequest:myRequest
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
and you are probably creating the NSURL request in the main thread. Hence while it waits for the semaphore to be released on the mainthread, the NSURL completion handler is waiting for the mainthread to be free of its current run loop. So create a new operation queue.
would it not be easier to do something like this instead:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //run on a background thread
while(!finishedLoadingAllData){
NSURLRequest *myRequest = [self loadData: startAt:startAt maxResults:maxResults];
NSHTTPURLResponse *response = nil;
NSError *error = nil;
NSData *responseData = [NSURLConnection sendSynchronousRequest:myRequest returningResponse:&response error:&error]; //blocks until completed
if(response.statusCode == 200 && responseData != nil){ //handle response and set finishedLoadingAllData when you want
//do stuff with response
dispatch_sync(dispatch_get_main_queue(), ^{
//do stuff on the main thread that needs to be done
}
}
});
Please dont do that.. NSURLConnection sendAsynchronousRequest will be loading itself in loop for you, if your data is in chunk.. try this instead..
__block NSMutableData *fragmentData = [NSMutableData data];
[[NSOperationQueue mainQueue] cancelAllOperations];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
[fragmentData appendData:data];
if ([data length] == 0 && error == nil)
{
NSLog(#"No response from server");
}
else if (error != nil && error.code == NSURLErrorTimedOut)
{
NSLog(#"Request time out");
}
else if (error != nil)
{
NSLog(#"Unexpected error occur: %#", error.localizedDescription);
}
else if ([data length] > 0 && error == nil)
{
if ([fragmentData length] == [response expectedContentLength])
{
// finished loading all your data
}
}
}];
I've created two chunky json response from server handling method.. And one of them is this, so hope this will be useful to you as well.. Cheers!! ;)
I started studying ObjectiveC not too long ago and I'm trying to write an app that has two UIImageViews and an array of URLs to load from web. After loading all the images from the URLs asynchronously, I would like to start animation on the UIImageViews that change pictures every 3 seconds. My only issue is that I don't want the animations until after all the images have been downloaded. the code I've got far is:
- (void)viewWillAppear:(BOOL)animated
{
self.imagesArray = [[NSMutableArray alloc] init];
[self initUrlArrayAndGetImages];
}
- (void)initUrlArrayAndGetImages
{
self.urlsArray = #[#"abc.com/1.png",
#"abc.com/2.png",
#"abc.com/3.png"];
for (int i = 0; i < [self.urlsArray count]; i++)
{
NSString *string = [self.urlsArray objectAtIndex:i];
NSURL *urlFromString = [NSURL URLWithString:string];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:urlFromString];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
if (!connectionError)
{
UIImage *imageFromData = [UIImage imageWithData:data];
[self.imagesArray addObject:imageFromData];
}
else
{
NSLog(#"%#, %#", connectionError, [connectionError userInfo]);
}
}];
}
So where will I call the method that activates the animation from? Because if I call it at the completion of the block it adds an image at a time (very fast, true, but still refreshes the method all the time...). What is my best solution?
Thanks in advance
You could add an if() statement checking to see if the count of images in the imagesArray is equal to the count of URLs in the urlsArray. If so, then call the animation. This would look like:
if (!connectionError)
{
UIImage *imageFromData = [UIImage imageWithData:data];
[self.imagesArray addObject:imageFromData];
if(self.imagesArray.count == self.urlsArray.count)
{
// All images are loaded successfully
// Activate animation
}
}
else
{
NSLog(#"%#, %#", connectionError, [connectionError userInfo]);
}
It's important to note that this will only work if all images were downloaded successfully. If one or more result in a connection error, it will not call the animation. To get around this, you could easily implement a dedicated count variable that is incremented when the completionHandler is called, whether or not there was a connection error. Then use this to compare to self.urlsArray.count.
In your completion handler you're adding the downloaded images to your self.imagesArray. Add code that compares imagesArray.count to urlsArray.count. If they are equal, all downloads have been completed and you can begin your animation:
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
if (!connectionError)
{
UIImage *imageFromData = [UIImage imageWithData:data];
[self.imagesArray addObject:imageFromData];
if (self.imagesArray.count == self.urlsArray.count)
{
//All downloads are complete. Trigger the animation
}
else
{
NSLog(#"%#, %#", connectionError, [connectionError userInfo]);
}
}];
I'm trying to make an asynchronous NSURL Request, but I'm getting all "FALSE."
-(BOOL)checkConnectionForHost:(NSString*)host{
BOOL __block isOnline = NO;
NSURLRequest *request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:host] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:1];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if([(NSHTTPURLResponse*)response statusCode]==200){
isOnline = TRUE;
}
}];
NSLog(#"%i",isOnline);
return isOnline;
}
Also, this code is being called "6" times when I'm actually just using it with a:
-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
and there are only 3 cells, or 3 items in my data source. First time dealing with async and callbacks in Objective-C, so a detailed answer would be much appreciated! Thanks!
Asynchronous calls will be executed in parallel, and its result will receive in the completion block. In your case, the return statement will be executed before the completion of the Asynchronous request. That will be always FALSE.
You should use Synchronous request for this, and handle not to Block the UI.
-(BOOL)checkConnectionForHost:(NSString*)host{
BOOL isOnline = NO;
NSURLRequest *request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:host] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:1];
NSHTTPURLResponse *response;
NSError *error;
NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
NSLog(#"Response status Code : %d",response.statusCode);
isOnline = response.statusCode == 200;
return isOnline;
}
You can use that method inside dispatch queues,
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^{
BOOL status = [self checkConnectionForHost:#"http://google.com"];
NSLog(#"Host status : %#",status ? #"Online" : #"Offline");
});
You should realize that this problem is inherently asynchronous. You can't solve it with a synchronous approach. That is, your accepted solution is just an elaborated and suboptimal wrapper which ends up being eventually asynchronous anyway.
The better approach is to use an asynchronous method with a completion handler, e.g.:
typedef void (^completion_t)(BOOL isReachable);
-(void)checkConnectionForHost:(NSString*)host completion:(completion_t)completionHandler;
You can implement is as follows (even though the request isn't optimal for checking reachability):
-(void)checkConnectionForHost:(NSString*)host
completion:(completion_t)completionHandler
{
NSURLRequest* request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:host]];
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (completionHandler) {
completionHandler(connectionError == nil && [(NSHTTPURLResponse*)response statusCode]==200);
}
}];
}
Please note:
Don't set a timeout as short as in your original code.
The completion handler will be called on a private thread.
Usage:
[self checkConnectionForHost:self.host completion:^(BOOL isReachable){
dispatch_async(dispatch_get_main_queue(), ^{
self.reachableLabel.text = isReachable ? #"" : #"Service unavailable";
});
}];
Your isOnline is probably being set to YES, but it's happening asynchronously. It is almost certainly executing after you log out the value of isOnline. So you should move your NSLog() call up into the block you pass as the handler to the asynchronous URL request.
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.