I'm using AFNetworking 2.0 to access a web api (although this would apply to NSURLSession as well), and currently I have a bunch of code that looks like this:
[self.rottenTomatoesManager GET:#"movies.json" parameters:#{#"q" : searchString, #"apikey" : [self.rottenTomatoesManager apiKey]}
success:^(NSURLSessionDataTask *task, id responseObject) {
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
if(response.statusCode == 200){
NSDictionary *responseData = responseObject;
self.searchResults = responseData[#"movies"];
[self.searchDisplayController.searchResultsTableView reloadData];
[self.tableView reloadData];
}
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error loading movies %#", error.localizedDescription);
}];
I'd like to take that functionality and wrap it in a convenience method that looks something like this
NSArray *results = [self.rottenTomatoesManager searchMoviesWithTitle:#"The avengers"]
to clean up the ViewController code and to make most of the code framework agnostic.
What is the best way to do this so that I'm not turning a nice asynchronous blocks based API into a synchronous API?
Callback blocks are great for this.
[self loadSomethingWithCallback:^(NSArray *results) {
NSLog(#"%#", results);
}];
Related
Evening, I'm working with the Marvel-API, trying to download all the characters.
To download all the characters you have to do multiple requests, in each request you can specified the limit and the offset.
So I've set the limit at the max of 100 and for every request I increase the offset by 100.
Doing that, I do infinite request. Of course.
So I thought that I should stop when the "results" array retrieved from the JSON object is empty.
So the logic should be good, I keep requesting characters 100 by 100 until there are no more to retrieve.
But of course working with networking and async code isn't always so easy. And obviously I got stocked.
I'm sure that the problems is in these lines of code:
#pragma mark - Requesting data
-(void)getData {
NetworkManager *networkManager = [NetworkManager alloc];
while(self.requestMustEnd == false) {
NSLog(#"offset: %d", networkManager.offset);
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:networkManager.getUrlPath parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) {
NSLog(#"JSON: %#", responseObject);
[self parseResponseData:responseObject];
} failure:^(NSURLSessionTask *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
[networkManager increaseOffset];
}
}
#pragma mark - Parsing Method
-(void)parseResponseData:(NSDictionary *)responseDictionary {
NSArray *marvelArray = [[responseDictionary objectForKey:#"data"] objectForKey:#"results"];
if (marvelArray.count == 0) {
self.requestMustEnd = true;
}
for(NSDictionary* marvel in marvelArray)
{
Character *currentMarvelEntity = [[Character alloc] initWithMarvel:marvel];
//NSLog(#"currentMarvelEntity %#", currentMarvelEntity.name);
[self.marvelCharacters addObject:currentMarvelEntity];
}
[self.tableView reloadData];
}
The key part to stop the request is:
if (marvelArray.count == 0) {
self.requestMustEnd = true;
}
But, still, it never end to request. it is not for the if condition, I'm sure. But probably because, having an async code, the getData func no matter what keep requesting data.
Any tips?
This post may help. Try:
[manager.operationQueue cancelAllOperations];
I have a networking class called: ITunesAlbumDataDownloader
#implementation AlbumDataDownloader
- (void)downloadDataWithURLString:(NSString *)urlString
completionHandler:(void (^)(NSArray *, NSError *))completionHandler
{
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:urlString]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
NSArray *albumsArray = [self parseJSONData:data];
completionHandler(albumsArray, error);
}];
[dataTask resume];
}
- (NSArray *)parseJSONData:(NSData *)data {
NSMutableArray *albums = [[NSMutableArray alloc] init];
...
...
// Return the array
return [NSArray arrayWithArray:albums];
}
#end
and i need to create a Unit Test for this which does the following:
The NSURLSession dataTaskWithRequest:completionHandler: response is mocked to contain the fake JSON data i have:
// Expected JSON response
NSData *jsonResponse = [self sampleJSONData];
The returned array from the public method downloadDataWithURLString:completionHandler: response should contain all the albums and nil error.
Other points to bare in mind is that i need to mock NSURLSession with the fake JSON data "jsonResponse" to the downloadDataWithURLString:completionHandler: method WITHOUT invoking an actual network request.
I have tried various different things but i just can not work it out, i think its the combination of faking the request and the blocks which is really confusing me.
Here is two examples of my test method that i tried (i actually tried a lot of other ways also but this is what i have remaining right now):
- (void)testValidJSONResponseGivesAlbumsAndNilError {
// Given a valid JSON response containing albums and an AlbumDataDownloaderTests instance
// Expected JSON response
NSData *jsonResponse = [self sampleJSONDataWithAlbums];
id myMock = [OCMockObject mockForClass:[NSURLSession class]];
[[myMock expect] dataTaskWithRequest:OCMOCK_ANY
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
{
}];
[myMock verify];
}
and
- (void)testValidJSONResponseGivesAlbumsAndNilError {
// Given a valid JSON response containing albums and an AlbumDataDownloaderTests instance
// Expected JSON response
NSData *jsonResponse = [self sampleJSONDataWithAlbums];
id myMock = [OCMockObject mockForClass:[AlbumDataDownloader class]];
[[[myMock stub] andReturn:jsonResponse] downloadDataWithURLString:OCMOCK_ANY
completionHandler:^(NSArray *response, NSError *error)
{
}];
[myMock verify];
}
}
I have a feeling that in both instances I'm probably way off the mark :(
I would really appreciate some help with this.
Thanks.
UPDATE 1:
Here is what i have now come up with but need to know if I'm on the right track or still making a mistake?
id mockSession = [OCMockObject mockForClass:[NSURLSession class]];
id mockDataTask = [OCMockObject mockForClass:[NSURLSessionDataTask class]];
[[mockSession stub] dataTaskWithRequest:OCMOCK_ANY
completionHandler:^(NSData _Nullable data, NSURLResponse Nullable response, NSError * Nullable error)
{
NSLog(#"Response: %#", response);
}];
[[mockDataTask stub] andDo:^(NSInvocation *invocation)
{
NSLog(#"invocation: %#", invocation);
}];
The trick with blocks is you need the test to call the block, with whatever arguments the test wants.
In OCMock, this can be done like this:
OCMStub([mock someMethodWithBlock:([OCMArg invokeBlockWithArgs:#"First arg", nil])]);
This is convenient. But…
The downside is that the block will be invoked immediately when someMethodWithBlock: is called. This often doesn't reflect the timing of production code.
If you want to defer calling the block until after the invoking method completes, then capture it. In OCMock, this can be done like this:
__block void (^capturedBlock)(id arg1);
OCMStub([mock someMethodWithBlock:[OCMArg checkWithBlock:^BOOL(id obj) {
capturedBlock = obj;
return YES;
}]]);
// ...Invoke the method that calls someMethodWithBlock:, then...
capturedBlock(#"First arg"); // Call the block with whatever you need
I prefer to use OCHamcrest's HCArgumentCaptor. OCMock supports OCHamcrest matchers, so I believe this should work:
HCArgumentCaptor *argument = [[HCArgumentCaptor alloc] init];
OCMStub([mock someMethodWithBlock:argument]);
// ...Invoke the method that calls someMethodWithBlock:, then...
void (^capturedBlock)(id arg1) = argument.value; // Cast generic captured argument to specific type
capturedBlock(#"First arg"); // Call the block with whatever you need
I am very new to the concept of asynchronous programming, but I get the general gist of it (things get run in the backround).
The issue I'm having is I have a method which utilizes AFNetworking 2.0 to make an HTTP post request, and it works for the most part.
However, I can't figure out how to get the method to actually return the value received from the response as the method returns and THEN gets the value from the response.
-(int) registerUser
{
self.responseValue = 000; //Notice I set this to 000 at start of method
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.requestSerializer = [AFJSONRequestSerializer serializer];
NSDictionary *parameters = #{ #"Username": #"SomeUsername" };
[manager POST:#"http://XXX/register"
parameters:parameters
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSLog(#"JSON: %#", responseObject);
NSError *err = nil;
self.responseValue = [[responseObject objectForKey:#"res"] intValue];
//Note: This^ value returns 99 and NSLogs confirm this
}
failure:^(AFHTTPRequestOperation *operation, NSError *err)
{
NSLog(#"Error: %#", err);
}];
return self.responseValue; //This will return 000 and never 99!
}
Whats the 'proper' way to handle this situation? I've heard whispers of using a 'callback', but I don't really understand how to implement that in this situation.
Any guidance or help would be awesome, cheers!
The issue is that the POST runs asynchronously, as you point out, so you are hitting the return line well before the responseValue property is actually set, because that success block runs later. Put breakpoints/NSLog statements in there, and you'll see you're hitting the return line first.
You generally do not return values from an asynchronous methods, but rather you adopt the completion block pattern. For example:
- (void)registerUserWithCompletion:(void (^)(int responseValue, NSError *error))completion
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.requestSerializer = [AFJSONRequestSerializer serializer];
NSDictionary *parameters = #{ #"Username": #"SomeUsername" };
[manager POST:#"http://XXX/register"
parameters:parameters
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSLog(#"JSON: %#", responseObject);
int responseValue = [[responseObject objectForKey:#"res"] intValue];
if (completion)
completion(responseValue, nil);
}
failure:^(AFHTTPRequestOperation *operation, NSError *err)
{
NSLog(#"Error: %#", err);
if (completion)
completion(-1, err); // I don't know what you want to return if it failed, but handle it appropriately
}];
}
And then, you could use it as follows:
[self registerUserWithCompletion:^(int responseValue, NSError *error) {
if (error)
NSLog(#"%s: registerUserWithCompletion error: %#", __FUNCTION__, error);
else
NSLog(#"%d", responseValue);
// do whatever you want with that responseValue here, inside the block
}];
// Needless to say, don't try to use the `responseValue` here, because
// `registerUserWithCompletion` runs asynchronously, and you will probably
// hit this line of code well before the above block is executed. Anything
// that is dependent upon the registration must called from within the above
// completion block, not here after the block.
Note, I'd suggest you retire that responseValue property you had before, because now that you're using completion blocks, you get it passed back to you via that mechanism, rather than relying on class properties.
Check this one and use search ;-))
Getting variable from the inside of block
its a lot of duplicates already!
:-)
I’m sorry if this question is too basic, but I can’t seem to find a an answer online.
I want to fetch the JSON result and have them returned with the class method below. But as you can see, by fetching the JSON in the block method, I don’t have a way to return them as result.
What is the correct way to to return them as NSDictionary from inside block method, or is there any other way to simplify this?
+ (NSDictionary *) fetchtPostsCount:(NSString *) count
page: (NSString *) page {
NSDictionary *requestParameter = [[NSDictionary alloc] initWithObjectsAndKeys:count, #"count", page, #"page", nil];
[[self sharedClient] GET:#"get_recent_posts"
parameters:requestParameter success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"%#", [responseObject objectForKey:#"posts"]);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"%#", error);
}];
return nil;
}
AFNetworking executes requests on different thread, and calls the success or failure block when its done. Conceptually, you can imagine that your fetchPostsCount method will have already completed and returned its value by the time request is finished.
You almost certainly want it to work that way. Running the request on another thread and NOT waiting for it, allows your main UI thread to continue processing events and rendering screen updates. You don't want to get in the way of those things, or the user (and iOS) will get unhappy.
However, if you insist on waiting for the request to complete before returning, you could set a flag to monitor the status of the request, and then wait on that flag until the request is complete:
BOOL requestComplete = NO;
id requestResponseObject = nil;
[[self sharedClient] GET:#"get_recent_posts"
parameters:requestParameter success:^(NSURLSessionDataTask *task, id responseObject) {
requestResponseObject = responseObject;
requestComplete = YES;
NSLog(#"%#", [responseObject objectForKey:#"posts"]);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
requestComplete = YES;
NSLog(#"%#", error);
}];
while (!requestComplete)
{
// Tie up the thread, doing nothing...
}
// Proceed
I started developing my application using AFNetworking. Everything went OK till I want to use core data. I know there is an additional class (AFIncrementalStore) for that. But because I'm new to IOS-development and there is not a lot of information about that. I decided to switch to RestKit because here is a lot more information. Now, I followed a tutorial about AFNetworking. Here I created an API class which this method in it.
+(API *)sharedInstance
{
static API *sharedInstance = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^ {
sharedInstance = [[self alloc]initWithBaseURL:[NSURL URLWithString:kAPIHost]];
});
return sharedInstance;
}
#pragma mark - init
//intialize the API class with the destination host name
-(API *)init
{
//call super init
self = [super init];
if (self != nil){
//initialize the object
user = nil;
[self registerHTTPOperationClass:[AFJSONRequestOperation class]];
// Accept HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
[self setDefaultHeader:#"Accept" value:#"application/json"];
}
return self;
}
-(void)loginCommand:(NSMutableDictionary *)params onCompletion:(JSONResponseBlock)completionBlock{
NSLog(#"%#%#",kAPIHost,kAPILogin);
NSMutableURLRequest *apiRequest = [self multipartFormRequestWithMethod:#"POST" path:kAPILogin parameters:params constructingBodyWithBlock:^(id <AFMultipartFormData>formData){
//TODO: attach file if needed
}];
AFJSONRequestOperation *operation = [[AFJSONRequestOperation alloc] initWithRequest:apiRequest];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject){
//success!
NSLog(#"SUCCESSSS!");
completionBlock(responseObject);
}failure:^(AFHTTPRequestOperation *operation, NSError *error){
//Failure
NSLog(#"FAILUREE!");
completionBlock([NSDictionary dictionaryWithObject:[error localizedDescription] forKey:#"error"]);
}];
[operation start];
}
This handles the communication between my webservice and application.
In the viewControler itself I call this method like this.
/* [[API sharedInstance] loginCommand:[NSMutableDictionary dictionaryWithObjectsAndKeys:_txtLogin.text,#"email",_txtPass.text,#"pwd", nil] onCompletion:^(NSDictionary *json){
//completion
if(![json objectForKey:#"error"]){
NSLog(#"status %#",[json valueForKeyPath:#"data.status"]);
if([[json valueForKeyPath:#"data.status"]intValue] == 200){
// Everything is oké, and login is succesfull
}else{
//show validation
}
}else {
NSLog(#"Cannot connect to the server");
}
}];*/
This is how I do this in AFnetworking. But what are the differences when I do this in RestKit. I searched for tutorials. But after the update from RestKit 1.0 to 2.0 a lot of these tutorials are outdated. So I hope anybody can help me out with this!
Kind regards!
I used this tutorial for using RestKit. It shows you how to use it and you can learn the other details. http://www.youtube.com/watch?v=dFi9t8NW0oY