I am converting my app routines from ASIHTTP to AFNetworking due to the unfortunate discontinuation of work on that project ... and what I found out later to be the much better and smaller codebase of AFNetworking.
I am finding several issues. My code for ASIHTTPRequest is built as a method. This method takes a few parameters and posts the parameters to a url ... returning the resulting data. This data is always text, but in the interests of making a generic method, may sometimes be json, sometimes XML or sometimes HTML. Thus I built this method as a standalone generic URL downloader.
My issue is that when the routine is called I have to wait for a response. I know all the "synchronous is bad" arguments out there...and I don't do it a lot... but for some methods I want synchronous.
So, here is my question. My simplified ASIHTTP code is below, followed by the only way i could think of coding this in AFNetworking. The issue I have is that the AFNetworking sometimes does not for the response before returning from the method. The hint that #mattt gave of [operation waitUntilFinished] totally fails to hold the thread until the completion block is called... and my other method of [queue waitUntilAllOperationsAreFinished] does not necessarily always work either (and does NOT result in triggering the error portion of the [operation hasAcceptableStatusCode] clause). So, if anyone can help, WITHOUT The ever-present 'design it asynchronously', please do.
ASIHTTP version:
- (NSString *) queryChatSystem:(NSMutableDictionary *) theDict
{
NSString *response = [NSString stringWithString:#""];
NSString *theUrlString = [NSString stringWithFormat:#"%#%#",kDataDomain,kPathToChatScript];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:[NSURL URLWithString:theUrlString]];
for (id key in theDict)
{
[request setPostValue:[theDict objectForKey:key] forKey:key];
}
[request setNumberOfTimesToRetryOnTimeout:3];
[request setAllowCompressedResponse:YES];
[request startSynchronous];
NSError *error = [request error];
if (! error)
{
response = [request responseString];
}
return response;
}
AFNetworking version
- (NSString *) af_queryChatSystem:(NSMutableDictionary *) theDict
{
NSMutableDictionary *theParams = [NSMutableDictionary dictionaryWithCapacity:1];
for (id key in theDict)
{
[theParams setObject:[theDict objectForKey:key] forKey:key];
}
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:kDataDomain]];
NSMutableURLRequest *theRequest = [httpClient requestWithMethod:#"POST" path:[NSString stringWithFormat:#"/%#",kPathToChatScript] parameters:theParams];
__block NSString *responseString = [NSString stringWithString:#""];
AFHTTPRequestOperation *operation = [[[AFHTTPRequestOperation alloc] initWithRequest:theRequest] autorelease];
operation.completionBlock = ^ {
if ([operation hasAcceptableStatusCode]) {
responseString = [operation responseString];
NSLog(#"hasAcceptableStatusCode: %#",responseString);
}
else
{
NSLog(#"[Error]: (%# %#) %#", [operation.request HTTPMethod], [[operation.request URL] relativePath], operation.error);
}
};
NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
[queue addOperation:operation];
[queue waitUntilAllOperationsAreFinished];
[httpClient release];
return responseString;
}
Thanks very much for any ideas.
- (void)af_queryChatSystem:(NSMutableDictionary *) theDict block:(void (^)(NSString *string))block {
...
}
Now within the completionBlock do:
block(operation.responseString);
block will act as the delegate for the operation. remove
-waitUntilAllOperationsAreFinished
and
return responseString
You call this like:
[YourInstance af_queryChatSystem:Dict block:^(NSString *string) {
// use string here
}];
Hope it helps. You can refer to the iOS example AFNetworking has
I strongly recommend to use this opportunity to convert to Apple's own NSURLConnection, rather than adopt yet another third party API. In this way you can be sure it won't be discontinued. I have found that the additional work required to get it to work is minimal - but it turns out to be much more robust and less error prone.
My solution is manually to run the current thread runloop until the callback have been processed.
Here is my code.
- (void)testRequest
{
MyHTTPClient* api = [MyHTTPClient sharedInstance]; // subclass of AFHTTPClient
NSDictionary* parameters = [NSDictionary dictionary]; // add query parameters to this dict.
__block int status = 0;
AFJSONRequestOperation* request = [api getPath:#"path/to/test"
parameters:parameters
success:^(AFHTTPRequestOperation *operation, id responseObject) {
// success code
status = 1;
NSLog(#"succeeded");
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// failure
status = 2;
NSLog(#"failed");
}];
[api enqueueHTTPRequestOperation:request];
[api.operationQueue waitUntilAllOperationsAreFinished];
while (status == 0)
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate date]];
}
STAssertEquals(status, 1, #"success block was executed");
}
Related
Does anyone know how to set ticket inside AFHTTPSessionOperation?
This is the previous call using AFNetworking framework 1.0
NSURLRequest* request = [self.myClient requestWithMethod:#"POST" path:[NSString stringWithFormat:#"%#/%#", controller, action] parameters:parameters];
AFHTTPRequestOperation* operation = [self.myClient HTTPRequestOperationWithRequest:request success:success failure:failure];
[self.mirrorClient enqueueHTTPRequestOperation:operation];
The ticket is stored inside the self.myClient. self.myClient.ticket
But I'm not sure how to implement that in the following call using AFHTTPSessionOperation with AFNetworking framework 3.1.
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] init];
AFHTTPRequestSerializer <AFURLRequestSerialization> * requestSerializer = manager.requestSerializer;
[requestSerializer setValue:[NSString stringWithFormat:#"%#", self.myClient.ticket] forHTTPHeaderField:#"Authorization"];
NSOperation *operation = [AFHTTPSessionOperation operationWithManager:manager HTTPMethod:#"POST"
URLString:urlString parameters:parameters
uploadProgress:nil downloadProgress: nil
success:success failure:failure];
Thank you
This code looks basically correct. You could simplify the requestSerializer configuration a tad, and I might not instantiate a new session for every request, but the following worked fine for me:
- (void)performRequest:(NSString *)urlString
parameters:(id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask *task, NSError *error))failure {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager.requestSerializer setValue:self.myClient.ticket forHTTPHeaderField:#"Authorization"];
NSOperation *operation = [AFHTTPSessionOperation operationWithManager:manager
HTTPMethod:#"POST"
URLString:urlString
parameters:parameters
uploadProgress:nil
downloadProgress:nil
success:success
failure:failure];
[self.queue addOperation:operation];
}
I watched it in Charles, and the ticket, 12345678 appeared in my request header, as expected:
I suspect your problem rests elsewhere. This code does set the Authorization header to ticket. Make sure this is the right place to set the ticket. Also, make sure the ticket is what you think it is.
Having an issue which is more of a design consideration than that of code.
My iOS app interfaces with a json web service. I am using AFNetworking and my issue is basically I need the init function (which authenticates the AFHTTPClient and retrieves a token) to complete entirely before I make any additional requests (that require said token).
From the code below, I would be interested in hearing design approaches to achieving this, I would prefer to keep all requests async an alternative solution would be to make the request in initWithHost:port:user:pass synchronous (not using AFNetworking) which I am aware is bad practice and want to avoid.
DCWebServiceManager.h
#import <Foundation/Foundation.h>
#import "AFHTTPClient.h"
#interface DCWebServiceManager : NSObject
{
NSString *hostServer;
NSString *hostPort;
NSString *hostUser;
NSString *hostPass;
NSString *hostToken;
AFHTTPClient *httpClient;
}
// Designated Initialiser
- (id)initWithHost:(NSString *)host port:(NSString *)port user:(NSString *)user pass:(NSString *)pass;
// Instance Methods
- (void)getFileList;
#end
DCWebServiceManager.m
#import "DCWebServiceManager.h"
#import "AFHTTPClient.h"
#import "AFHTTPRequestOperation.h"
#import "AFJSONRequestOperation.h"
#implementation DCWebServiceManager
- (id)initWithHost:(NSString *)host port:(NSString *)port user:(NSString *)user pass:(NSString *)pass
{
self = [super init];
if (self)
{
hostServer = host;
hostPort = port;
hostUser = user;
hostPass = pass;
NSString *apiPath = [NSString stringWithFormat:#"http://%#:%#/", hostServer, hostPort];
httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:apiPath]];
[httpClient setAuthorizationHeaderWithUsername:hostUser password:hostPass];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:#"authenticate.php" parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject){
// Do operations to parse request token to be used in
// all requests going forward...
// ...
// ...
// Results in setting: hostToken = '<PARSED_TOKEN>'
NSLog(#"HostToken: >>%#<<", hostToken);
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
NSLog(#"ERROR: %#", operation.responseString);
}];
[operation start];
}
return self;
}
- (void)getFileList
{
// *************************
// The issue is here, getFileList gets called before the hostToken is retrieved..
// Make the authenticate request in initWithHost:port:user:pass a synchronous request perhaps??
// *************************
NSLog(#"IN GETFILELIST: %#", hostToken); // Results in host token being nil!!!
NSString *queryString = [NSString stringWithFormat:#"?list&token=%s", hostToken];
NSMutableURLRequest *listRequest = [httpClient requestWithMethod:#"GET" path:queryString parameters:nil];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:listRequest success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON){
NSLog(#"SUCCESS!");
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON){
NSLog(#"ERROR!: %#", error);
}];
[operation start];
}
#end
ViewController.m
....
DCWebServiceManager *manager = [[DCWebServiceManager alloc] initWithHost:#"localhost" port:#"23312" user:#"FOO" pass:#"BAR"];
[manager getFileList];
// OUTPUTS
IN GETFILELIST: (nil)
HostToken: >>sdf5fdsfs46a6cawca6<<
....
...
I'd suggest subclassing AFHTTPClient and adding a +sharedInstance and property for the token.
+ (MyGClient *)sharedInstanceWithHost:(NSString *)host port:(NSString *)port user:(NSString *)user pass:(NSString *)pass {
static MyClient *sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[... your code from the init ...]
});
return sharedInstance;
}
You can then override enqueueHTTPRequestOperationWithRequest:success:failure to check for the token before enqueueing further operations.
Additionally, you can collect the operations and enqueue them as soon as the token is set by overriding the setter for the property.
Like #patric.schenke said, you can subclass AFHTTPClient if you want to clean up some of your code, but the real issue is that you need to make another request to authenticate (if your token is nil) before making the request to getFileList.
I would recommend using blocks in the same way that AFNetworking is using blocks to remain asynchronous. Move your HTTP call into its own method and call it only when your hostToken is nil:
- (void)getFileList
{
if (self.token == nil) {
[self updateTokenThenWhenComplete:^(void){
// make HTTP call to get file list
}];
} else {
// make HTTP call to get file list
}
}
- (void)updateTokenThenWhenComplete:(void (^))callback
{
//... make HTTP request
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject){
self.token = responseObject.token;
callback();
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
//...
}];
}
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];
}
I have 2 questions.
1.) I am creating a NSObject class, and i am having the following code in it. (ASIHTTPRequest POST).
The name of the NSObject class is called, SendToServer. I call the class as follows;
SendToServer *sv = [[SendToServer alloc]];
sv.grabURLInTheBackground ;
NSLog(#"This line is executed ");
The following is the code that is in the SendToServer NSObject class.
- (void)grabURLInTheBackground
{
if (![self queue]) {
[self setQueue:[[[NSOperationQueue alloc] init] autorelease]];
}
NSURL *url = [NSURL URLWithString:#"http://allseeing-i.com"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request setDidFinishSelector:#selector(requestDone:)];
[request setDidFailSelector:#selector(requestWentWrong:)];
[[self queue] addOperation:request]; //queue is an NSOperationQueue
}
- (void)requestDone:(ASIHTTPRequest *)request
{
NSString *response = [request responseString];
}
- (void)requestWentWrong:(ASIHTTPRequest *)request
{
NSError *error = [request error];
}
The problem is that, the code executes the line sv.grabURLInTheBackground ; and before it executes the requestDone or requestWentWrong methods, it executes the NSLog (NSLog(#"This line is executed "); )
What i want my program to do is to complete all the operations in the SendToServer NSObject class and then Execute the NSLog (In a sequence).
First execute sv.grabURLInTheBackground ; once all the activities in that method/class is over, then return to the code and execute the other line which is NSLog(#"This line is executed "); .
2.) I need to return a String when the requestDone method is executed. How do i modify the code to do so;
- (NSString * )requestDone:(ASIHTTPRequest *)request {
}
but how do i edit [request setDidFinishSelector:#selector(requestDone:)];, for the above code ?
------------------------------------------------------------------------------------------------------------------------------------------
EDIT
I am doing this for user login. Upon button click i will be calling the grabURLInTheBackground method from the NSObject class. And the viewcontroller needs to know if the user login was successful or failed.
SendToServer *sv = [[SendToServer alloc]];
[sv grabURLInTheBackground] ;
NSLog(#"User login SUcess or failed %#", [sv userloginSucessOrFail]);
For example say [sv userloginSucessOrFail] returns if the user login was success or failed.
What hapence here, is that after [sv grabURLInTheBackground] is called, it directly goes and executes the NSLog(#"User login SUcess or failed %#", [sv userloginSucessOrFail]); line of code.
What i want is, i need to find a way to let my ViewCOntroller know if the user login was a Success or failure.
First: call init on your object.
Second: grabURLInTheBackground is a method not a property. It should by called with square brackets
So you code becomes:
SendToServer *sv = [[SendToServer alloc] init];
[sv grabURLInTheBackground];
NSLog(#"This line is executed ");
To accomplish point 1) you need to make a synchronous request
NSURL *url = [NSURL URLWithString:#"http://allseeing-i.com"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request startSynchronous];
NSError *error = [request error];
if (!error) {
NSString *response = [request responseString];
}
The problem is that if this code is executed on the main thread is blocking (not good)...
For the second point. You can't.
EDIT:
What you have to do is something like the following steps:
Before calling grabURLInTheBackground you have to notify the user that a request is pending.. like putting an UIActivityIndicator, or disabling the UI,...
when you receive the callback then update the UI: hide the activity indicator, re-enable the UI... or if the request failed, notify the user.
This may sounds weird but please bear with me. I have 6-7 API calls which make request to a server one by one. I want to implement these calls in a separate thread. But when I do this, none of my delegate methods (of NSURLConnection) gets called even after managing a separate NSRunloop
([[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];)
Can anyone suggests me alternative approach for the same or any correction in existing functionality??
Use ASIHTTPRequest instead. It's much easier to use than NSURLConnection.
a quick google threw up this: http://blog.emmerinc.be/index.php/2009/03/15/multiple-async-nsurlconnections-example/ he is using a dictionary to manage multiple requests
Using a separate thread for each NSURLConnection, which is already multithreaded is a bad idea. It's just pointlessly using system resources and defeating NSURLConnections attempts to manage connections optimally. However, it does work so if you are not receiving delegate messages you are doing something wrong. Rather than find an alternative way todo it you should try to get to the bottom of your problem with the runloop.
I'm using ASIHTTPRequest to do the similar operation. Go through the following code change the downloadAllIcons method to suit your requirement,
[NSThread detachNewThreadSelector:#selector(downloadAllIcons:) toTarget:self withObject:xmlData];
-(void) downloadAllIcons:(NSData *)_xmlData
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSArray *IconList= PerformXMLXPathQuery(_xmlData,#"//icon");
[[NSUserDefaults standardUserDefaults] setObject:IconList forKey:#"IconList"];
for (int i=0; i<[IconList count]; i++) {
if ([[NSUserDefaults standardUserDefaults] objectForKey:[[IconList objectAtIndex:i] objectForKey:#"nodeContent"]]==nil) {
NSData * responseData=[self downloadProccessedImage:[[IconList objectAtIndex:i] objectForKey:#"nodeContent"]];
if(responseData)
[[NSUserDefaults standardUserDefaults] setObject:responseData forKey:[[IconList objectAtIndex:i] objectForKey:#"nodeContent"]];
//NSLog(#"%#",[[IconList objectAtIndex:i] objectForKey:#"nodeContent"]);
}
}
[pool release];
}
-(id) downloadProccessedImage:(NSString *)_URL
{
NSData *response=nil;
NSURL *url = [NSURL URLWithString:_URL];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setTimeOutSeconds:60];
[ASIHTTPRequest setShouldThrottleBandwidthForWWAN:YES];
[request startSynchronous];
NSError *error = [request error];
if (!error)
{
response = [request responseData];
}
return response;
}