iPhone: ASIHTTP setDidFinishSelector issue - iphone

I have the following code:
- (void)imageDownloaded:(ASIHTTPRequest *) request idPlace:(NSNumber *)idPlace
NSData *responseData = [request responseData];
if (responseData == nil) {
NSLog(#"bad");
}
}
And
NSURL *url = [NSURL URLWithString:[item objectForKey:#"image"]];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request setDidFinishSelector:[self performSelector:#selector(imageDownloaded:idPlace:)withObject:request withObject:place.idPlace]];
But it doesn't works because the responseData is null.
I have tried this way (with a selector with only one argument):
[request setDidFinishSelector:#selector(imageDownloaded:)];
- (void)imageDownloaded:(ASIHTTPRequest *) request
And this works. But i need to know the idPlace in order to save the image with the respective name to get later the image.
Any idea about how can i do this? Since selector with two arguments doesn't work i cannot find the solution to my question.

This line is garbage:
[request setDidFinishSelector:[self performSelector:#selector(imageDownloaded:idPlace:)withObject:request withObject:place.idPlace]];
This should be throwing a compiler warning at the very least. You're executing -imageDownloaded:idPlace: immediately, and then taking the result (which in your case is going to be whatever garbage value was on the stack) and returning that as the selector. the response data is nil because you're actually executing your imageDownloaded: method before the request even runs.
Assuming ASIHTTPRequest only accepts didFinishSelectors of the form foo: (e.g. one argument), then there's no good way to do what you're trying to do, which is to attach a second argument onto the method. That simply won't work. You have a few alternatives, but the simplest one is probably to make use of ObjC Associated Objects. You can hang your place.idPlace object off of the request and get at it later.
At the top of your file, define a key:
static char kAssociationKey; // we just want its memory location
Then when you start the request you can run:
objc_setAssociatedObject(request, &kAssociationKey, place.idPlace, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[request setDidFinishSelector:#selector(imageDownloaded:)];
This will attach place.idPlace to the request object. Then in your didFinishSelector you can run
id oldIdPlace = objc_getAssociatedObject(request, &kAssociationKey);

You can use
[request setTag:place.idPlace]
to set Place ID
and
[request tag]
to get it in your imageDownloaded

Related

check which request is which from NSURLConnection delegate

What is the best way to check which request is which inside the delegate method:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
}
Right now I have a NSURLConnection that I set to the NSURLConnection before making a request and inside didReceiveResponse I do:
if (self.tempConnection == connection)
however there is a possiblity this won't work for race conditions. Is there a better way to do this?
There is a better way in OS5. Forget about all those bothersome delegate messages. Let the connection build the data for you, and put your finished code right in line with your start code:
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.site.com"]];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
NSLog(#"got response %d, data = %#, error = %#", [httpResponse statusCode], data, error);
}];
I've looked at a bunch of different ways to do this, and I've found that by far the cleanest and easiest in order to manage is to use a block pattern. That way you are guaranteed to be responding to the right request upon completion, avoid race conditions, and you don't have any issues with variables or objects going out of scope during the asynchronous call. It's also a lot easier to read/maintain your code.
Both ASIHTTPRequest and AFNetworking APIs provide a block pattern (however ASI is no longer supported so best to go with AFNetworking for new stuff). If you don't want to use one of these libraries, but want to do it yourself, you can download the source for AFNetworking and review their implementation. However, that seems like a lot of extra work for little value.
Consider creating a separate class to serve as the delegate. Then, for each NSURLConnection spawned, instantiate a new instance of the delegate class to for that NSURLConnection
Here's some brief code to illustrate this:
#interface ConnectionDelegate : NSObject <NSURLConnectionDelegate>
...then implement the methods in the .m file
Now, I'm guessing you probably have the code you posted in a UIViewController subclass (or some other class serving different purposes)?
Wherever you are kicking off the requests, use this code:
ConnectionDelegate *newDelegate = [[ConnectionDelegate alloc] init];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"<url here">]];
[NSURLConnection connectionWithRequest:request delegate:newDelegate];
//then you can repeat this for every new request you need to make
//and a different delegate will handle this
newDelegate = [[ConnectionDelegate alloc] init];
request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"<url here">]];
[NSURLConnection connectionWithRequest:request delegate:newDelegate];
// ...continue as many times as you'd like
newDelegate = [[ConnectionDelegate alloc] init];
request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"<url here">]];
[NSURLConnection connectionWithRequest:request delegate:newDelegate];
You might consider storing all the delegate objects in a NSDictionary or some other data structure to keep track of them. I'd consider using an NSNotification in connectionDidFinishLoading to post a notification that the connection is done, and to serve whatever object created from the response. Lemme know if you want code to help you visualize that. Hope this helps!

Implementation of Async Request causing many leaks

I've inherited a project that uses of ASIHttpRequest for all network communication. I am unclear as to which specific version we're using. All I can tell is that, from the .h files, the oldest creation date on a particular file is 17/08/10 (ASIDataDecompressor).
We're using completion and failure blocks. For some reason, the failure block is often triggered, which should only really happen if the server fails to respond. Our logs look sane, and we haven't received any notifications (Airbrake) that there were server problems around the time the errors occur, so for now I'm moving forward with the assumption that our server is fine and it's the app that is the culprit.
I decided to run the app through Instruments (Leaks) and was astonished to see that when I force a request to fail, ~27 leaks are created immediately. I'm don't know how to get around Instruments all that well, so I'm not really sure what to do with the information now that I have it.
I figured I'd post my code to see if there's anything glaring.
In viewDidLoad, this code is executed
[[MyAPI sharedAPI] getAllHighlights:pageNumber:perPage onSuccess:^(NSString *receivedString,NSString *responseCode) {
[self getResults:receivedString];
if(![responseCode isEqualToString:#"Success"]) {
[self hideProgressView];
appDelegate.isDiscover_RefreshTime=YES;
[[MyAPI sharedAPI] showAlert:responseCode];
} else {
NSString *strLogEvent=#"Discover_Highlights_Loaded Page_";
strLogEvent=[strLogEvent stringByAppendingFormat:#"%i",intPageNumber];
[FlurryAnalytics logEvent:strLogEvent timed:YES];
}
} onFail:^(ASIFormDataRequest *request) {
NSDictionary *parameters = [[MyAPI sharedAPI] prepareFailedRequestData:request file:#"Discover" method:_cmd];
[FlurryAnalytics logEvent:#"Unable_to_Connect_to_Server" withParameters:parameters timed:true];
[self hideProgressView];
appDelegate.isDiscover_RefreshTime=YES;
[[AfarAPI sharedAPI] showAlert:#"Unable to Connect to Server."];
[tblHighlightsGrid reloadData];
[tblListHighlights reloadData];
}];
These typedefs have been defined at the top of API Singleton:
typedef void (^ASIBasicBlockWrapper)(NSString *responseString,NSString *responseCode);
typedef void (^ASIBasicBlockWrapperFail)(ASIFormDataRequest *request);
MyAPISingleton#getAllHighlights...
- (void)getAllHighlights:(NSString *)pageNumber:(NSString *)perPage onSuccess:(ASIBasicBlockWrapper)cb1 onFail:(ASIBasicBlockWrapperFail)cb2{
NSString *access_token= [[NSUserDefaults standardUserDefaults] objectForKey:#"access_token"];
NSString *url = [baseURL stringByAppendingFormat:AFAR_GET_ALL_HIGHLIGHTS_ENDPOINT, pageNumber,perPage];
if (access_token) { url = [url stringByAppendingFormat:ACCESS_TOKEN, access_token]; }
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:[NSURL URLWithString:url]];
[request setRequestMethod:#"GET"];
[request setDelegate:self];
[self executeAsynchronousRequest:request onSuccess:cb1 onFail:cb2];
}
And finally, MyAPI#executeAsynchronousRequest:
- (void) executeAsynchronousRequest:(ASIFormDataRequest *)request onSuccess:(ASIBasicBlockWrapper)cb1 onFail:(ASIBasicBlockWrapperFail)cb2
{
[request setCompletionBlock:^{
int statusCode = [request responseStatusCode];
NSString *statusMessage = [self statusErrorMessage:statusCode];
cb1([request responseString],statusMessage);
}];
[request setFailedBlock:^{
cb2(request);
}];
[request startAsynchronous];
}
Does anything stand out as to why 27 leaks are created?
I figured this out.
The ASIHttpRequest Documentation is very clear about the fact that you need to designate your request object with the __block storage mechanism:
Note the use of the __block qualifier when we declare the request, this is important! It tells the block not to retain the request, which is important in preventing a retain-cycle, since the request will always retain the block.
In getAllHighlights(), I'm doing that, but then I'm sending my request object as an argument to another method (executeAsyncRequest). The __block storage type can only be declared on local variables, so in the method signature, request is just typed to a normal ASIFormDataRequest, and so it seems as though it loses its __block status.
The trick is to cast (I'm not sure if that's technically accurate) the argument before using it in a block.
Here's my leak free implementation of executeAsyncRequest:
- (void) executeAsyncRequest:(ASIFormDataRequest *)request onSuccess:(ASIBasicBlockWrapper)cb1 onFail:(ASIBasicBlockWrapperFail)cb2
{
// this is the important part. now we just need to make sure
// to use blockSafeRequest _inside_ our blocks
__block ASIFormDataRequest *blockSafeRequest = request;
[request setCompletionBlock: ^{
int statusCode = [blockSafeRequest responseStatusCode];
NSString *statusMessage = [self statusErrorMessage:statusCode];
cb1([blockSafeRequest responseString],statusMessage);
}];
[request setFailedBlock: ^{
cb2(blockSafeRequest);
}];
[request startAsynchronous];
}

Need to log storage type of local variable

In short, I need to know if I am able to log the storage type for a variable.
Specifically, I want to log whether a variable has the __block storage type modifier applied to it.
Ideally, I'm looking for something like:
NSLog(#"storage type: %#", [localVar storageType]);
In case you're wondering, I think I just figured out a memory leak I've been debugging for the past few days, and I want to test if my assumption is correct.
I'm using ASIHttpRequest with setCompletionBlock and setFailedBlock, but I'm passing my request object to a convenience method that does the actual setup of the blocks, like so:
- (void)getAllHighlights:success:(ASIBasicBlockWrapper)cb1 fail:(ASIBasicBlockWrapperFail)cb2{
// blah blah blah
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setRequestMethod:#"GET"];
[request setDelegate:self];
[self executeAsynchronousRequest:request onSuccess:cb1 onFail:cb2];
}
Then, executeAsynchronousRequest sets up the Blocks and starts the request:
- (void) executeAsynchronousRequest:(ASIFormDataRequest *)request onSuccess:(ASIBasicBlockWrapper)cb1 onFail:(ASIBasicBlockWrapperFail)cb2
{
[request setCompletionBlock:^{
int statusCode = [safeRequest responseStatusCode];
NSString *statusMessage = [self statusErrorMessage:statusCode];
cb1([safeRequest responseString],statusMessage);
}];
[request setFailedBlock:^{
cb2(safeRequest);
}];
[request startAsynchronous];
}
My hunch tells me that even though I set up my request object as __block ASIFormDataRequest *request, when it's used within executeAsynchronousRequest, it's lost the __block storage type since it has only been typed as (ASIFormDataRequest *)request.
Thanks!
you aren't modifying request in a block, so __block isn't going to do anything for you... if you were passing in request to a block, it wouldn't be copied, it would keep the locally scoped version when you passed it into the block.

use two ASIHTTPRequest in the same class

if i use ASIHTTPRequest the first time , with asynchroun mode , i recive result and error in
(void)requestFinished:(ASIHTTPRequest *)request
and the error in
(void)requestFailed:(ASIHTTPRequest *)request
but what if I make another query asynchronously? I will receive the result in the same method? how to know if this is the result of the first query or second? I tryed to change the delegate but it don't work
-(void)getCities
{
NSString * myURLString = [NSString stringWithFormat:#"http://localhost:8080/Data/resources/converter.city/CountryCode/%#",choosedCodeCity];
NSURL *url =[NSURL URLWithString:myURLString];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:resultCities];
[request startAsynchronous];
}
-(void)resultCities :(ASIHTTPRequest *)request
{
}
you can do:
request.didFinishSelector = #selector(yourFinishMethodHere:);
request.didFailSelector = #selector(yourFailMethodHere:);
in other words, you do not have to use the default "requestFinished:" and "requestFailed:" methods on the delegate.
ASIHttpRequest has a NSDictionary* userInfo property that you can use to store whatever you like. You can simply add a flag to this dictionary in order to tell the two requests apart.
Another approach would be to use the ASIHttpRequest methods that take blocks instead of using a delegate.
EDIT:
To use the flag approach, when you create the request object, do something like
[request.userInfo setObject:[NSNumber numberWithInt:1] forKey:#"flag"];
Then in the response methods you can ask the request for the flag to determine which request it is.
int flag = [[request.userInfo objectForKey:#"flag"] intValue];

Delaying, cancelling or replacing a ASIFormDataRequest in a NSOperationQueue (for a searchbox)

I'm successfully making a ASIFormDataRequest using the below code.
//get groups
if (![self queue]) {
[self setQueue:[[[NSOperationQueue alloc] init] autorelease]];
}
//make the url by appending the URL from the Constant class to the jsp name
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"%#/%#", URL, #"connectors/searchGroupsServlet.jsp"]];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request addRequestHeader:#"User-Agent" value:USER_AGENT];
[request addPostValue:[login username] forKey:#"username"];
[request addPostValue:[login password] forKey:#"password"];
[request addPostValue:[searchText lowercaseString] forKey:#"query"];
[request addPostValue:GROUP_FILTER_LIMIT forKey:#"limit"];
[request setDelegate:self];
[request setDidFinishSelector:#selector(requestDone:)];
[request setDidFailSelector:#selector(requestWentWrong:)];
This request is currently made on every key press a user makes in a searchbox (The text typed is sent off in the request as the search string). However, rather than sending the request on every key press, I want to delay the request by a second to allow users to type further characters into the searchbox before the request is sent.
I've successfully made a thread that waits a second as users continue to type (although admittedly Im not convinced this is the best way to do it yet, but it works for now)...
this
[self performSelectorInBackground:#selector(wait:) withObject:request];
calls this
-(void)wait:(NSString *)request
{
[NSThread sleepForTimeInterval:1.00];
[[self queue] addOperation:request]; //queue is an NSOperationQueue
}
but, if a user continues to type, I haven't managed to work out how to cancel the request or not put the request in the queue, or empty the queue and replace it with the new request.
Finally, obviously I could force users to wait until they have pressed the 'search' button on the pop-up keyboard, but I was hoping to provide search results without that.
Thanks
The answer was to create an NSTimer, and invalidate it whenever a new key press had been made. Then start it again.
[timer invalidate];
You can try this to cancel
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anArgument