I have this class that performs a REST call .
- (NSString*)cerca:(NSString*)email{
if ([[Utility sharedUtility] connected]) {
HttpClient *self = [HttpClient sharedHTTPClient];
[self.requestSerializer setValue:[self getUserAgent] forHTTPHeaderField:#"User-Agent"];
[self.requestSerializer setValue:NST_CODE forHTTPHeaderField:#"Nst-Code"];
[self.requestSerializer setValue:[[NSUserDefaults standardUserDefaults] objectForKey:#"nst_id"] forHTTPHeaderField:#"Nst-Id"];
[self.requestSerializer setValue:[[NSUserDefaults standardUserDefaults] objectForKey:#"api_key"] forHTTPHeaderField:#"Api-Key"];
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
parameters[#"email"] = email;
__block NSString *result;
[self POST:#"get_info" parameters:parameters
success:^(NSURLSessionDataTask *task, id responseObject) {
result = responseObject;
} failure:^(NSURLSessionDataTask *task, NSError *error) {
}];
return result;
}
The call returns a JSON . My problem is that when I do return result ; Nothing is passed to the calling method .
Can you tell me why ??
The calling method is
HttpClient *client = [HttpClient alloc];
NSString *result = [client cerca:email];
That method you are calling is an asynchronous call, meaning the result comes after you have returned. You need to change your method to accept a block argument and return the result in the callback.
- (void)cerca:(NSString*)email callback:(void (^)(id result)) callback {
if ([[Utility sharedUtility] connected]) {
...
[self POST:#"get_info" parameters:parameters
success:^(NSURLSessionDataTask *task, id responseObject) {
if (callback) {
callback(responseObject)
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
}];
}
}
To call the method you would do:
[client cerca:email completion:^(NSString *response) {
// Do what you want with the response.
}];
Its because the self POST call is asynchronous and you are returning the result before it has had time to be assigned. You need to rebuild your - (NSString*)cerca:(NSString*)email method so it somehow can handle the asynchronousness of this. Easiest way is usually to change the method so it takes a completion block as in parameter. Maybe something like:
- (void)cerca:(NSString *)email completion:(void (^)(NSString *res))completion {
[self POST:#"get_info" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
if (completion) {
completion(responseObject);
}
}failure:^(NSURLSessionDataTask *task, NSError *error) {
if (completion){
completion(nil);
}
}];
}
I changed my method
- (NSString*)cerca:(NSString*)email completion:(void (^)(NSString *res))completion {
But I do not understand what to call it . Indications ?
[client cerca:email completion:nil];
Related
I'm posting a notification in the request failure block:
[manager POST:path
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (operation.response.statusCode == 200) {
//message delegate
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[[NSNotificationCenter defaultCenter] postNotificationName:HOST_UNREACHABLE object:operation];
}];
In the method that receives the notification, the completionBlock attribute is nil.
How do I access it without subclassing & overriding?
First to answer the question of how to send a block to an NSNotification:
The way you're attempting to do it is dangerous, because we don't know how AFHTTPSessionManager handles the blocks you pass it, and, unless its in the public interface, what it does may not remain fixed over time.
So, make a local variable to represent the block you want to pass, say, the completionBlock...
// this is a local variable declaration of the block
void (^completionBlock)(AFHTTPRequestOperation*,id) = ^(AFHTTPRequestOperation *operation, id response) {
if (operation.response.statusCode == 200) {
//message delegate
}
};
[manager POST:path
parameters:nil
success:completionBlock
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[[NSNotificationCenter defaultCenter] postNotificationName:HOST_UNREACHABLE object:completionBlock];
}];
The observer can get the block and invoke it this way...
- (void)didReceiveNotification:(NSNotification *)notification {
void (^completionBlock)(AFHTTPRequestOperation*,id) = notification.object;
// use it
[manager POST:someOtherPath
parameters:nil
success:completionBlock
// etc.
];
}
But I think the approach is strange. It spreads out responsibility for making the request to the object that gets notified, leaving it needing to know the path to retry the parameters (which you don't have in this case, but you might one day).
Consider instead subclassing the manager and adding behavior that does the retry. Now your manager subclass can be in charge of all requests, including retries, and your other classes are just customers who handle the outcome. Something like...
#interface MyAFHTTPRequestManager : AFHTTPSessionManager
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
retryURL:(NSString *)retryURLString
parameters:(nullable id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask *task, NSError *error))failure;
#end
Have your subclass implementation call super with the first URLString, and upon failure, call super with the retryURLString. e.g.
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
retryURL:(NSString *)retryURLString
parameters:(nullable id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask *task, NSError *error))failure {
[super POST:URLString parameters:parameters success:success
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[super POST:retryURLString parameters:parameters success:success failure:failure];
}];
}
I'm moving my app code to an MVC model and so I created a method to retrieve some data from an API.
+ (NSMutableArray *)loadFromFeed {
NSString *feed = #"https://api.test.com";
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:feedUrl]];
request = [mutableRequest copy];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [JSONResponseSerializerWithData serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSArray *jsonArray = (NSArray *)[responseObject objectForKey:#"items"];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
CLS_LOG(#"Error");
}];
}
Now, ideally, I'd like to return jsonArray as part of this method. However, since AFHTTPRequestOperation is asynchronous, I don't know how to solve this and still be able to call [Data loadFromFeed]; anywhere in the app. How can I do this?
You could pass two block named success and failure to loadFromFeed ,
and then call the two block from your setCompletionBlockWithSuccess success and failure block, passing jsonArray to the success block:
typedef void (^Success)(id data);
typedef void (^Failure)(NSError *error);
- (void)loadFromFeed:(Success)success failure:(Failure)failure;
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSArray *jsonArray = (NSArray *)[responseObject objectForKey:#"items"];
success?success(jsonArray):nil;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
failure?failure(error):nil;
}];
then use in this way:
[Data loadFromFeed:^(id data) {
NSLog(#"%#",data)
} failure:^(NSError *error) {
NSLog(#"%#",error)
}];];
I got a async problem with my code. I got all of my webrequests in 1 class. One of my requests needs to return an NSMutableArray that another class needs to use. My webRequest code is here:
- (NSMutableArray*) getTournamentsInClub:(NSString *)clubGUID withDelegateViewController:(UIViewController *)viewController {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSMutableArray *responseArray = [[[NSMutableArray alloc] init] autorelease];
NSString *URL = [[NSString alloc]initWithFormat:#"SomeURL=%#",clubGUID];
[manager POST:URL parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
for (id obj in responseObject){
//NSLog(#"obj: %#",[obj valueForKey:#"CustomerName"]);
[responseArray addObject:obj];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
return responseArray;
}
I call the method from a viewController like this:
[self handleClubTournaments:[[TournamentsWebService sharedToursWS] getTournamentsInClub:
//Show load screen. (hide in handler function)
GP_MobilAppDelegate *xdelegate = [[UIApplication sharedApplication] delegate];
[xdelegate showLoadingScreen:self.clubToursTableView andStatus:NSLocalizedString(#"loadTours", #"")];
And my handleClubTournaments function looks like this:
-(void) handleClubTournaments:(id)result {
GP_MobilAppDelegate *xdelegate = [[UIApplication sharedApplication] delegate];
if([result isKindOfClass: [NSError class]]) {
// If an error has occurred, handle it
[xdelegate hideLoadingScreen];
[[TournamentsWebService sharedToursWS] showErrorMessageAccordingToFault:result];
return;
}
if([result isKindOfClass: [SoapFault class]]) {
[xdelegate hideLoadingScreen];
// If a server error has occurred, handle it
[[TournamentsWebService sharedToursWS] showErrorMessageAccordingToFault:result];
return;
}
//Do something with result...
if ([result count] > 0) {
NSLog(#"Antal klubturneringer: %d", [result count]);
//Start by removing excisting tours
[self.tournamentsSourceArray removeAllObjects];
NSMutableArray *tempArray=[NSMutableArray array];
for (GGTournamentData *t in result) { //cast object in result list and add them to array
[tempArray addObject:t];
}
self.tournamentsSourceArray = [self sortByStringDate:tempArray]; //sort by date
[tempArray release];
NSLog(NSLocalizedString(#"tourLoadet", #""));
}
[self.clubToursTableView reloadData];
[xdelegate hideLoadingScreen];
//Scroll view
if (self.tournamentsSourceArray.count > 0) { //hvis det er turneringer..
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:[self findIndexOfMonthClosestToDate]];
[self.clubToursTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
}
So my problem is that the NSMutableArray gets returned before my async task is done. I know a async task behaves like that, but how do i make sure that my handleClubTournaments function don't run before my webrequest(getTournamentsInClub) got some data for it?
Thanks in advance.
I don't think you know how Asynchronous operations work. The NSMutableArray will never be set, because it is returned synchronously.
In your case, I suggest you to work with delegates.
- (void)getTournamentsInClub:(NSString *)clubGUID withDelegateViewController:(UIViewController *)viewController completionBlock:(void (^)(NSMutableArray *result))completionBlock {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSMutableArray *responseArray = [[[NSMutableArray alloc] init] autorelease];
NSString *URL = [[NSString alloc]initWithFormat:#"SomeURL=%#",clubGUID];
[manager POST:URL parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
for (id obj in responseObject) {
[responseArray addObject:obj];
}
// Request finished. Call the block.
completionBlock(responseArray);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
- (void)handleClubTournaments {
GP_MobilAppDelegate *xdelegate = [[UIApplication sharedApplication] delegate];
[[TournamentsWebService sharedToursWS] getTournamentsInClub:^(NSMutableArray *result)
{
// Hide the Loading Indicator. Do something with the Result.
}];
// You can't access the result synchronously, therefore it's impossible to depend on it synchronously.
}
another way to return the data asynchronously would be blocks, similar to the AFNetworking solution.
You can read more about getting started with blocks here and how to use delegates here.
I'm trying to create a function that will return a value based on the response from an AFNetworking POST request.
However, I can't figure out a way to do this, as the function is asynchronous, so the value is returned before the response is actually received.
int didLogin;
__block NSString *response;
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:#"text/html"];
NSDictionary *parameters = #{#"username":username, #"password":password};
[manager POST:loginUrl parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
//json "response" object from server response
response = [responseObject objectForKey:#"response"];
NSLog(#"Response: %#", response);
[HUD hide:YES];
[HUD removeFromSuperViewOnHide];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[HUD hide:YES];
[HUD removeFromSuperViewOnHide];
NSLog(#"Login Error: %#", error);
}];
if ([response compare:#"1"] == NSOrderedSame || [response compare:#"2"] == NSOrderedSame)
{
//successful login/1-new device/2-existing device
didLogin = 1;
}
else if ([response compare:#"0"] == NSOrderedSame)
{
//unsuccessful login/invalid password
didLogin = 0;
}
else{
//unsuccessful login
didLogin = 2;
}
At which point it would return didLogin.
Is there any way to make this work, or do I need to use a synchronous request?
Your method should take a callback block as a parameter and then you should call that block with didLogin when the asynchronous process is complete. You need to embrace the asynchronous nature of what you're trying to do throughout your code.
I'm testing a method using OCMock. The method is as follows:
- (NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(NSDictionary *)parameters
block:(void (^)(id responseObject, NSError *error))block
{
return [self GET:URLString parameters:parameters success:^(NSURLSessionDataTask * __unused task, id responseObject) {
block(responseObject, nil);
} failure:^(NSURLSessionDataTask * __unused task, NSError *error) {
block(nil, error);
}];
}
This test fails with "expected method was not invoked":
id sessionManagerPartialMock = [OCMockObject partialMockForObject:[FOOHTTPSessionManager manager]];
NSString *URLstring = #"test";
NSDictionary *parameters = nil;
void (^block)(id, NSError *) = ^(id responseObject, NSError *error) {};
void (^successBlock)(NSURLSessionDataTask *, id) = ^(NSURLSessionDataTask * __unused task, id responseObject) {
block(responseObject, nil);
};
void (^failureBlock)(NSURLSessionDataTask *, NSError *) = ^(NSURLSessionDataTask * __unused task, NSError *error) {
block(nil, error);
};
[[sessionManagerPartialMock expect] GET:URLstring parameters:parameters success:successBlock failure:failureBlock];
[sessionManagerPartialMock GET:URLstring parameters:parameters block:block];
[sessionManagerPartialMock verify];
[sessionManagerPartialMock stopMocking];
But this passes:
id sessionManagerPartialMock = [OCMockObject partialMockForObject:[FOOHTTPSessionManager manager]];
NSString *URLstring = #"test";
NSDictionary *parameters = nil;
void (^block)(id, NSError *) = ^(id responseObject, NSError *error) {};
[[sessionManagerPartialMock expect] GET:URLstring parameters:parameters success:[OCMArg isNotNil] failure:[OCMArg isNotNil]];
[sessionManagerPartialMock GET:URLstring parameters:parameters block:block];
[sessionManagerPartialMock verify];
[sessionManagerPartialMock stopMocking];
Why does the first test fail and how can I make it pass?
I've put an example project on GitHub demonstrating the issue: https://github.com/paulyoung/OCMockExample
As others have said, blocks are compared by comparing their addresses. Two different blocks - even though they do the exact same thing - are not equal.
You can still make your test more specific by actually invoking the success and failure block, and check if they behave as expected.
As far as I understand you want to test that
the method calls another method with two blocks, that when invoked, invoke the original block with
some response object as the first and nil as the second argument in case of success
nil as the first, an NSError* as the second argument in case of failure
Here is the test for the success case:
id responseMock = #"responseObject";
__block BOOL blockCalled = NO;
void (^block)(id, NSError *) = ^(id responseObject, NSError *error) {
XCTAssertNil(error, #"");
XCTAssertEqualObjects(responseObject, responseMock, #"");
blockCalled = YES;
};
[[[sessionManagerPartialMock expect] andDo:^(NSInvocation *invocation) {
void (^successBlock)(NSURLSessionDataTask *, id);
[invocation getArgument:&successBlock atIndex:4];
successBlock((id)#"task", responseMock);
}] GET:URLstring parameters:parameters success:[OCMArg isNotNil] failure:[OCMArg isNotNil]];
[sessionManagerPartialMock GET:URLstring parameters:parameters block:block];
[sessionManagerPartialMock verify];
XCTAssert(blockCalled, #"");
I checked your repo and I think now I can give you good answer.
What you are trying to do in your test is blocks comparing and currently this is not possible in Obj-c world. Even if you block from test and block created inside singleton looks identical they are not the same blocks.
You can fix this by using you fail and success block as private properties and use them in your OCMock expect. Like :
[[sessionManagerPartialMock expect] GET:URLstring parameters:parameters success:sessionManager.successBlock failure:sessionManager.failureBlock];