Best practice for making async calls sync in iOS?? - iphone

I have a singleton class, APIClient, which needs to have userId and authToken set up before it can make calls to my backend.
We are currently storing userId and authToken in NSUserDefaults. For fresh installs, these values do not exist and we query the server for them.
Currently, we have code in our ViewControllers' viewDidLoad methods that manually query the server if these values do not exist.
I am interested to make this class "just work". By this, I mean have the client check if it has been initialized, if not fire a call to the server and set the appropriate userId and authToken - all without manual interference.
This has proven to be a rather tricky due to:
I can't make asyncObtainCredentials synchronous because I was told by folks at #iphonedev that the OS will kill my app if I have to freeze the main thread for a network operation
For what we have right now, the first call will always fail because of the asynchronous nature of asyncObtainCredential. Nil will be returned and first calls will always fail.
Does anyone know of a good work around for this problem?
`
#interface APIClient ()
#property (atomic) BOOL initialized;
#property (atomic) NSLock *lock;
#end
#implementation APIClient
#pragma mark - Methods
- (void)setUserId:(NSNumber *)userId andAuthToken:(NSString *)authToken;
{
self.initialized = YES;
[self clearAuthorizationHeader];
[self setAuthorizationHeaderWithUsername:[userId stringValue] password:authToken];
}
#pragma mark - Singleton Methods
+ (APIClient *)sharedManager {
static dispatch_once_t pred;
static APIClient *_s = nil;
dispatch_once(&pred, ^{
_s = [[self alloc] initWithBaseURL:[NSURL URLWithString:SERVER_ADDR]];
_s.lock =[NSLock new] ;
});
[_s.lock lock];
if (!(_s.initialized)) {
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
NSNumber *userId = #([prefs integerForKey:KEY_USER_ID]);
NSString *authToken = [prefs stringForKey:KEY_AUTH_TOKEN];
// If still doesn't exist, we need to fetch
if (userId && authToken) {
[_s setUserId:userId andAuthToken:authToken];
} else {
/*
* We can't have obtainCredentials to be a sync operation the OS will kill the thread
* Hence we will have to return nil right now.
* This means that subsequent calls after asyncObtainCredentials has finished
* will have the right credentials.
*/
[_s asyncObtainCredentials:^(NSNumber *userId, NSString *authToken){
[_s setUserId:userId andAuthToken:authToken];
}];
[_s.lock unlock];
return nil;
}
}
[_s.lock unlock];
return _s;
}
- (void)asyncObtainCredentials:(void (^)(NSNumber *, NSString *))successBlock {
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:SERVER_ADDR]];
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:[OpenUDID value], #"open_udid", nil];
NSMutableURLRequest *request = [client requestWithMethod:#"GET" path:#"/get_user" parameters:params];
AFJSONRequestOperation *operation = \
[AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
...
// Do not use sharedManager here cause you can end up in a deadlock
successBlock(userId, authToken);
} failure:^(NSURLRequest *request , NSURLResponse *response , NSError *error , id JSON) {
NSLog(#"obtain Credential failed. error:%# response:%# JSON:%#",
[error localizedDescription], response, JSON);
}];
[operation start];
[operation waitUntilFinished];
}

You should check if those values are there in NSUserDefaults during the application launch. If they are not there, make a call to fetch it from server and show a loading overlay on the screen. Once you have fetched it, you can proceed with the next step.
If you dont want to use loading overlay, you can set some isLoading flag in APIClient class and check that to know if the asyn is still fetching. So whenever you are making a service call and you need these values, you know that how to handle it based this flag. Once you have got the required values and stored in NSUserDefaults, you can proceed with the next step. You can use Notifications/Blocks/KVO to notify your viewcontrollers to let them know that you have fetched these values.

Related

performSelector enters method on all instances, but only one thread finishes

This may be a naive question, but I'll ask it anyway as I cannot find any documentation that clears up this issue in my head.
I'm running iOS5.1 both on device and in the simulator with Xcode45-DP4.
I have a loop that iterates over an array of a number of instances of a class. In that loop I use performSelector on the instances to start a thread that does some relatively slow network operations — pulling down data that I'd rather do in the background.
[arrayOfFriends enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
Friend *f = (Friend*)obj;
iOSSLog(#"%d", idx);
[f performSelectorInBackground:#selector(showDescription) withObject:nil];
-(void)fetchTwitterStatus
{
iOSSLog(#"Trying to fetch twitterstatus %# %#", self.hash, self.twitterUserName);
[mLocalTwitterUser fetchTwitterAPIUserStatusWithScreenName:twitterUserName
withCompletionHandler:^(NSArray *arrayOfStatus, NSError *error) {
if(error) {
iOSSLog(#"%#", error);
} else {
iOSSLog(#"Got twitterstatus %# %d", self.twitterUserName, [arrayOfStatus count]);
#synchronized(items) {
[items addObjectsFromArray:arrayOfStatus];
}
}
}];
}
In my test case there are four instances. Each instance gets its selector, you know..selected. The first three definitely start but only the last actually completes as indicated by the log line that says "Got twitterstatus..." Which is weird.
I can also verify that the method the selector calls "fetchTwitterStatus"
What is the little fundamental nugget of multithreading that I'm missing here?
EDIT: here's fetchTwitterAPIUserStatusWithScreenName...quite a bit here, but effectively it's calling the Twitter API Endpoint user_timeline with a JSON response.
- (void)fetchTwitterUserStatusWithScreenName:(NSString *)screenname
excludeReplies:(BOOL)excludeReplies
withCompletionHandler:(OtterTwitterSearchHandler)completionHandler
{
self.twitterAPIStatusHandler = completionHandler;
//self.fetchTwitterUserStatusHandler = completionHandler;
NSString *urlString = [NSString stringWithFormat:#"https://api.twitter.com/1/statuses/user_timeline.json?screen_name=%#&include_rts=true&include_entities=true&exclude_replies=%#&count=50", screenname, excludeReplies?#"true":#"false"];
NSURL *url = [NSURL URLWithString:urlString];
#warning this isn't the way to do it - just checking the cache for refresh of the scroller
[[ASIDownloadCache sharedCache]removeCachedDataForURL:url];
iOSSRequest *request = [[iOSSRequest alloc] initWithURL:url
parameters:nil
requestMethod:iOSSRequestMethodGET];
NSMutableDictionary *oauthParams = [NSMutableDictionary dictionary];
[oauthParams setObject:[[Twitter sharedService] apiKey] forKey:kASIOAuthConsumerKey];
[oauthParams setObject:[[Twitter sharedService] apiSecret] forKey:kASIOAuthConsumerSecret];
[oauthParams setObject:[self oAuthAccessToken] forKey:kASIOAuthTokenKey];
[oauthParams setObject:kASIOAuthSignatureMethodHMAC_SHA1 forKey:kASIOAuthSignatureMethodKey];
[oauthParams setObject:#"1.0" forKey:kASIOAuthVersionKey];
[oauthParams setObject:[self oAuthAccessTokenSecret] forKey:kASIOAuthTokenSecretKey];
request.oauth_params = oauthParams;
[request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if (error) {
if (self.twitterAPIStatusHandler) {
self.twitterAPIStatusHandler(nil, error);
self.twitterAPIStatusHandler = nil;
}
} else {
NSMutableArray *recentStatusForTwitterUser = [[NSMutableArray alloc]init];
NSArray *array = [Twitter JSONFromData:responseData];
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
TwitterStatus *twitterStatus = nil;
twitterStatus = [[TwitterStatus alloc]initWithDictionary:obj];
[recentStatusForTwitterUser addObject:twitterStatus];
}];
if (self.twitterAPIStatusHandler) {
self.twitterAPIStatusHandler(recentStatusForTwitterUser, nil);
self.twitterAPIStatusHandler = nil;
}
}
}];
}
I'd suggest using the asynchronous abstractions already provided where possible. It would be a fairly rare/unique situation where you need to deal with threads directly.
I've found treating each network-based background task as a synchronous NSOperation on a queue works really well.
Get a new instance of NSOperationQueue, configure it, add tasks to it, and manage the queue. The benefit of this approach is that each task can be implemented as a simple synchronous task, and the queue takes care of concurrency. Optionally you can set dependencies (this task must complete before that one).
What is the little fundamental nugget of multithreading that I'm
missing here?
That taking non-multithreaded code and spinning off a random number of threads by performing an arbitrary method in the background is doomed to failure.
Concurrency is a design pattern that must be carefully considered from the start (or is a monumental refactoring effort).
First, you don't want to spawn a thread per network connection. Secondly, given that these are just HTTP requests, you would want to use the systems built in classes for asynchronous HTTP communications. Finally, your concurrency model must exactly specify how you are keeping all data in isolation until you hit whatever mechanism you are using to synchronize the data back into the central store.
Hard to say where that code is going off the rails without seeing more information.

AFNetworking authentication with web service

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){
//...
}];
}

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];
}

Converting from ASIHTTPRequest to AFNetworking

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");
}

NSOperationQueue and ASIHTTPRequest

I'm writing test cases for a wrapper class written around ASIHTTPRequest. For reasons I can't determine, my test cases complete with failure before the ASIHTTPRequest finishes.
Here's how the program flow works.
Start in my test case.
Init my http engine object, instruct it to create a new list
Create the new ASIHTTPRequest object and set it up.
Add the request to an operation queue.
Wait until that queue is empty
Check to see if my delegate methods were called and fail the test if they weren't.
Now, most of the time everything works fine and the test passes, but some of the time it fails because my delegate methods were called AFTER the operation queue returned control to my wait method.
Test Case
// Set my flags to 'NO'
- (void)setUp {
requestDidFinish = NO;
requestDidFail = NO;
}
- (void)testCreateList {
NSString *testList = #"{\"title\": \"This is a list\"}";
JKEngine *engine = [[JKEngine alloc] initWithDelegate:self];
NSString *requestIdentifier = [engine createList:jsonString];
[self waitUntilEngineDone:engine];
NSString *responseString = responseString_;
[engine release];
GHAssertNotNil(requestIdentifier, nil);
GHAssertTrue(requestDidFinish, nil);
GHAssertTrue([responseString hasPrefix:#"{\"CreateOrEditListResult\""], nil);
}
// Puts the test into a holding pattern until the http request is done
- (void)waitUntilEngineDone:(JKEngine *)engine {
[engine waitUntilFinishedRunning];
}
// The delegate method called on successful completion
- (void)requestFinished:(NSString *)requestIdentifier withResponse:(NSString *)response {
NSLog(#"request did finish");
requestDidFinish = YES;
responseIdentifier_ = [requestIdentifier retain];
responseString_ = [response retain];
}
Engine Code
- (NSString *)createList:(NSString *)list {
ASIHTTPRequest *request = [[ASIHTTPRequest alloc] initWithURL:[NSURL URLWithString:url]];
[request addRequestHeader:#"Content-Type" value:kContentType];
[request setRequestMethod:kPOST];
request.delegate = self;
[request appendPostData:[list dataUsingEncoding:NSUTF8StringEncoding]];
NSString *requestIdentifier = [NSString stringWithNewUUID];
[operationQueue_ addOperation:request];
[operationDictionary_ setObject:request forKey:requestIdentifier];
return requestIdentifier;
}
// This is the ASIHTTPRequest delegate method that's called on success
// but it sometimes isn't called until AFTER the operationQueue finishes running
- (void)requestFinished:(ASIHTTPRequest *)request {
DLog([request responseString]);
BOOL canNotifiyDelegate = [self.delegate respondsToSelector:#selector(requestFinished:withResponse:)];
if (canNotifiyDelegate) {
NSArray *keyArray = [operationDictionary_ allKeysForObject:request];
NSString *requestIdentifier = [keyArray objectAtIndex:0];
[operationDictionary_ removeObjectForKey:requestIdentifier];
if ([keyArray count] != 1) {
ALog(#"It looks like a request was added to the operation dictionary multiple times. There's a bug somewhere.", nil);
}
[self.delegate requestFinished:requestIdentifier withResponse:[request responseString]];
}
}
- (void)waitUntilFinishedRunning {
[operationQueue_ waitUntilAllOperationsAreFinished];
}
This is the way ASIHTTPRequest works. Delegate methods are called on the main thread, and calls to delegates do not block the request thread, so it's perfectly possible your delegates will be called after the queue finishes.
ASIHTTPRequest calls delegate methods on the main thread, by default GH-Unit runs its tests on a background thread. I'm still a little hazy on exactly what was going on, but forcing my network tests to run on the main thread fixed the problem.
I implemented the following method in my network test class.
- (BOOL)shouldRunOnMainThread {
return YES;
}