- (void)loadItems {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager.requestSerializer setValue:#"text/html" forHTTPHeaderField:#"Content-Type"];
[manager GET:#"someurl"
parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self reloadData];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
- (void)textFieldDidChange {
[_filteredArray removeAllObjects];
[self loadItems];
}
I am trying to implement instant search by making an API call every time a character changes. Since, the first few calls have less letters, they return more results, making the first few async calls finish slower than than the last few, meaning that if I type in hello quickly, I will end up getting the search results for h instead of the whole word since the last call to finish is the one for h. I need to keep the order of these calls, and make sure that the last query is not overwritten. I understand that I must use a queue structure. However doing something like this in textFieldDidChange doesn't seem to work:
dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
[self loadItems];
});
dispatch_group_notify(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
[self reloadData];
});
I think I need to use some sort of combination of dispatch_group_enter(group); and dispatch_group_leave(group);. However I still can't get the calls to stop overwriting the last call. I'm not sure if there is also a way to just cancel out all the other started calls with the last one, or if I have to wait for all of them to finish in order. Any help would be appreciated.
This was my solution. I just ended up using a counter that I pass into my loadItems function. While that counter updates, the async call still has its own value in it, so I just compare the two, and make sure to only reloadData if the async call's counter is equal to the latest one.
- (void)loadItems:(int)queryInt {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager.requestSerializer setValue:#"text/html" forHTTPHeaderField:#"Content-Type"];
[manager GET:#"someurl"
parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (searchQueryCounter - 1 == queryInt) {
[self reloadDatawithAnimation];
} else {
return;
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
- (void)textFieldDidChange {
[_filteredArray removeAllObjects];
[self loadItems:searchQueryCounter];
searchQueryCounter = searchQueryCounter + 1;
}
You might better address this by canceling the prior requests, not only preventing prior requests reporting results, but also ensuring that system resources are not consumed by requests that are no longer needed:
#interface ViewController () <UITextFieldDelegate>
#property (nonatomic, strong) AFHTTPRequestOperationManager *manager;
#property (nonatomic, weak) NSOperation *previousOperation;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// no need to instantiate new request operation manager each time;
// do it at some logical point of initialization (e.g. in `viewDidLoad`
// for view controllers, etc.).
self.manager = [AFHTTPRequestOperationManager manager];
[self.manager.requestSerializer setValue:#"text/html" forHTTPHeaderField:#"Content-Type"];
}
- (void)loadItems {
[self.previousOperation cancel];
typeof(self) __weak weakSelf = self; // probably should use weakSelf pattern, too
NSOperation *operation = [self.manager GET:#"someurl" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[weakSelf reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if ([error.domain isEqualToString:NSURLErrorDomain] && [error code] != NSURLErrorCancelled) {
NSLog(#"Error: %#", error);
}
}];
self.previousOperation = operation;
}
- (void)textFieldDidChange {
[_filteredArray removeAllObjects];
[self loadItems];
}
#end
I actually worked on a very similar problem last week and came up with an approach you might find useful.
I submit each request using performSelector:withObject:afterDelay with a slight delay (I've been experimenting with values ranging from 0.5 to 1.0 seconds. Somewhere from .66 to .75 seems like a good compromise value.)
With each new request, I cancel the previous pending performSelector call. That way nothing gets sent until the user stops typing for a short period of time. It's not perfect, but it reduces the amount of useless queries for word fragments. The code looks something like this:
static NSString *methodWord = nil;
[[self class] cancelPreviousPerformRequestsWithTarget: self
selector: #selector(handleWordEntered:)
object: methodWord];
methodWord = word;
[self performSelector: #selector(handleWordEntered:)
withObject: methodWord
afterDelay: .667];
The method handleWordEntered: actually sends the request to the server.
If the user types a letter, then another letter in less than 2/3 second, the previous pending request is cancelled and a new request is set to fire 2/3 of a second later. As long as the user keeps typing letters every 2/3 second, nothing is sent. As soon as the user pauses more than 2/3 second, a request is sent. Once the performSelector:withObject:afterDelay fires it can't be cancelled any more, so that request goes to the network and the reply is parsed.
Related
I am working on iOS App, and I am using AFNetworking for interacting with server API.
My issue is I want to send call and don't want to restrict user until response get from server, so issue is crash. When user move back to that particular screen lets say I have listing screen where I am getting data which is taking 6-7 seconds and meanwhile user move back to previous screen and when data come from API and call back that delete to listing screen but user move backed to that screen then App crashes
Here below is code for fetching data call.
+ (void) getRequestForDocumentListing:(NSDictionary *)headerParams urlQuery: (NSString*)action parameters:(NSDictionary*)params
onComplete:(void (^)(id json, id code))successBlock
onError:(void (^)(id error, id code))errorBlock
{
NSString *authorizationValue = [self setAuthorizationValue:action];
NSString *selectedLanguage = [ApplicationBaseViewController getDataFromDefaults:#"GLOBALLOCALE"];
NSString *language = selectedLanguage;
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
//set headers values
[manager.requestSerializer setValue:#"application/json" forHTTPHeaderField:#"Accept"];
[manager.requestSerializer setValue:language forHTTPHeaderField:#"Accept-Language"];
[manager.requestSerializer setValue:authorizationValue forHTTPHeaderField:#"authorization"];
[manager.requestSerializer setValue:#"x-folder" forHTTPHeaderField:#"inbox"];
[manager GET:action parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"document listing success");
NSInteger statusCode = [operation.response statusCode];
NSNumber *statusObject = [NSNumber numberWithInteger:statusCode];
successBlock(responseObject, statusObject);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSInteger statusCode = [operation.response statusCode];
NSNumber *statusObject = [NSNumber numberWithInteger:statusCode];
id responseObject = operation.responseData;
id json = nil;
id errorMessage = nil;
if (responseObject) {
json = [NSJSONSerialization JSONObjectWithData:responseObject options:kNilOptions error:&error];
errorMessage = [(NSDictionary*)json objectForKey:#"Message"];
}else{
json = [error.userInfo objectForKey:NSLocalizedDescriptionKey];
errorMessage = json;
}
errorBlock(errorMessage, statusObject);
}];
}
What I need is to stop call in ViewdidDisappear View delegate
- (AFHTTPRequestOperation *)GET:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:#"GET" URLString:URLString parameters:parameters success:success failure:failure];
[self.operationQueue addOperation:operation];
return operation;
}
How to solve this particular issue?
I got your point, I think the problem is not about the AFNetWorking or download, it is about how you organize your view controllers.
In short, you need to make sure the synchronization of the data and view.
What cause your crash is when users do some operation(eg. delete, move...), the data is not the same with what view shows.
Let's play back an example:
An array with 12 objects and show it with a table view.
User call a web request to change the array. As we know, it needs time.
User leave and come back again. In this view, table view shows with the old array.
At this point, web request comes back. The array is modified to 10 object.But at this time, the call back dose not cause the table view to load the new data.
When user do some operation, just like delete the 11st object in the table view. Actually, there is no 11st object in array.
So crash comes.
How to deal with it is to keep the synchronization of the data and view.
First get a reference to the Operation object by
AFHTTPRequestOperation *operation = [manager GET:action parameters:nil success:^...blah blah blah...];
Then you can set the completion block to nil when you move away from this screen.
[operation setCompletionBlock:nil];
Please note that even though you move away from the screen, the request may actually execute successfully. However, your app will not crash now.
Thanks RuchiraRandana and childrenOurFuture for your answer, I got help from your answers and finally I come to solution where I am not going to cancel operation and set nil delegate, because my others operation are also in working which is trigger on other screen.
I create a just BOOL and set YES default value in singleton class and also set to no in - (void)dealloc on that particular class and in API class where I am triggering that delegate I added that check.
if ([SHAppSingleton sharedInstance].isDocListControllerPop == YES) {
[delegate documentListResponse:documentList andStatusCode:code];
}
I know this might not be perfect solution but this resolved my issue.
Thanks
In the viewDidLoad method of my viewController I wrote something like this:
- (void)viewDidLoad {
[super viewDidLoad];
self.hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
self.hud.labelText = #"Loading";
[serverData loadNewsData:self ];
}
Basically I show the HUD and start a method loadNewsData on another class, passing the viewController.
The method in the other class is like this:
-(void)loadNewsData:(LoadViewController*) loadViewController
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:[siteAddress stringByAppendingString:getNewsList] parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
loadViewController.hud.detailsLabelText = #"updating news";
[NewsModel setNewsData:responseObject];
loadViewController.hud.detailsLabelText = #"Finished";
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
When the request of the AFHTTPRequestOperation starts I was expecting to see the sub menu in the HUD to change in "Updating news". But nothing happens. I just see Finished at the end of the process. The requests takes like 10sec so I was expecting to see "updating news", but nothing.
I try also to this:
dispatch_async(dispatch_get_main_queue(), ^{
loadViewController.hud.detailsLabelText = #"updating news"; });
Thinking I have to be in the UI queue to update some UI stuff, but it did not work either.
I'm a newbie probably I miss something.
thanks for any good advices.
Try to move loadViewController.hud.detailsLabelText = #"updating news"; before you start GET request. If you keep this inside the success block you won't see the text.
This link should help you out... NSURLConnection
It describes the NSURLConnection delegate method to use and how to use it for the exact situation your in. I understand that this isn't an AFNetworking solution but if your still looking for an AFNetworking solution, check out this method - (void)setDownloadProgressBlock:(void (^)(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead))block;.
Hope this gets you in the right track.
After some successful HTTP request I want to return totalArray in a callback, but only after all the request has been successful. After doing some research, I found that I should somehow use something like grand central dispatch or NSLock, but I'm not sure how to use them. How can I do this?
+ (void)httpRequestOnParameters:(NSURL*)url parametersArray:(NSArray*)parametersArray success:(Success)success failure:(Failure)failure {
NSMutableArray* totalArray = [NSMutableArray new];
for (NSDictionary* parameter in parametersArray) {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:[url absoluteString]
parameters:parameter
success:^(AFHTTPRequestOperation *operation, id responseObject) {
[totalArray addObject:responseObject];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
}
success(totalArray);
}
You could try using a counter set to 0 before the first async call. Then in the Success code block, increment the counter and place an if block checking for when the counter reaches your desired number of async calls.
counter = 0;
numAsyncCalls = x;
array = [ ];
asyncCall {
mainCode: ...
success: counter ++;
array.push(callResult);
if (counter == numAsyncCalls){
''Execute desired code on
''populated array[]
}
}
Does that make sense? Sorry if this post looks janky, I'm trying out the stack exchange app on my iPad.
I have a unit test in which I need to wait for an async task to finish. I am trying to use NSConditionLock as it seems to be a pretty clean solution but I cannot get it to work.
Some test code:
- (void)testSuccess
{
loginLock = [[NSConditionLock alloc] init];
Login login = [[Login alloc] init];
login.delegate = self;
// The login method will make an async call.
// I have setup myself as the delegate.
// I would like to wait to the delegate method to get called
// before my test finishes
[login login];
// try to lock to wait for delegate to get called
[loginLock lockWhenCondition:1];
// At this point I can do some verification
NSLog(#"Done running login test");
}
// delegate method that gets called after login success
- (void) loginSuccess {
NSLog(#"login success");
// Cool the delegate was called this should let the test continue
[loginLock unlockWithCondition:1];
}
I was trying to follow the solution here:
How to unit test asynchronous APIs?
My delegate never gets called if I lock. If I take out the lock code and put in a simple timer it works fine.
Am I locking the entire thread and not letting the login code run and actually make the async call?
I also tried this to put the login call on a different thread so it does not get locked.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[login login];
});
What am I doing wrong?
EDIT adding login code. Trimmed do the code for readability sake. Basically just use AFNetworking to execute a POST. When done will call delegate methods.
Login make a http request:
NSString *url = [NSString stringWithFormat:#"%#/%#", [_baseURL absoluteString], #"api/login"];
[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (_delegate) {
[_delegate loginSuccess];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (_delegate) {
[_delegate loginFailure];
}
}];
The answer can be found in https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFHTTPRequestOperation.m.
Since you are not setting the completionQueue property of the implicitly created AFHTTPRequestOperation, it is scheduling the callbacks on the main queue, which you are blocking.
Unfortunately, many answers (not all) in the given SO thread ("How to unit test asynchronous APIs?") are bogus and contain subtle issues. Most authors don't care about thread-safity, the need for memory-barriers when accessing shared variables, and how run loops do work actually. In effect, this leads to unreliable and ineffective code.
In your example, the culprit is likely, that your delegate methods are dispatched on the main thread. Since you are waiting on the condition lock on the main thread as well, this leads to a dead lock. One thing, the most accepted answer that suggests this solution does not mention at all.
A possible solution:
First, change your login method so that it has a proper completion handler parameter, which a call-site can set in order to figure that the login process is complete:
typedef void (^void)(completion_t)(id result, NSError* error);
- (void) loginWithCompletion:(completion_t)completion;
After your Edit:
You could implement your login method as follows:
- (void) loginWithCompletion:(completion_t)completion
{
NSString *url = [NSString stringWithFormat:#"%#/%#", [_baseURL absoluteString], #"api/login"];
[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (completion) {
completion(responseObject, nil);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (completion) {
completion(nil, error);
}
}];
Possible usage:
[self loginWithCompletion:^(id result, NSError* error){
if (error) {
[_delegate loginFailure:error];
}
else {
// Login succeeded with "result"
[_delegate loginSuccess];
}
}];
Now, you have an actual method which you can test. Not actually sure WHAT you are trying to test, but for example:
-(void) testLoginController {
// setup Network MOCK and/or loginController so that it fails:
...
[loginController loginWithCompletion:^(id result, NSError*error){
XCTAssertNotNil(error, #"");
XCTAssert(...);
<signal completion>
}];
<wait on the run loop until completion>
// Test possible side effects:
XCTAssert(loginController.isLoggedIn == NO, #""):
}
For any other further steps, this may help:
If you don't mind to utilize a third party framework, you can then implement the <signal completion> and <wait on the run loop until completion> tasks and other things as described here in this answer: Unit testing Parse framework iOS
I am doing a simple GET request with AFNetworking
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:#"http://someapi.com/hello.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"JSON: %#", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
Once I have made the request I want to be able to access the responseObject from any other method in the class.
I want to be able to save the responseObject so I can do something like display the output in a tableview.
It's common to creat object models that will be represented by JSON. When you get the response you would then parse the data into the models. The approach we use is to return the response to the requester through a completion block. You don't have to parse the JSON into strongly typed objects, but it really is helpful long term. It's probably a good idea to farm out the network request operations into a separate class (called a service) as well. This way you can instantiate a new service and get notified through a completion block that it is finished. For example your service's request signature could look like this:
typedef void(^HelloWorldCompletionHandler)(NSString *helloWorld, NSError *error);
- (void)requestHelloWorldData:(HelloWorldCompletionHandler)completionHandler;
// implementation
- (void)requestHelloWorldData:(HelloWorldCompletionHandler)completionHandler {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:#"http://someapi.com/hello.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
id JSONResponse = [operation responseObject];
if (operation.error) {
completionHandler(nil, error);
} else {
// parse the response to something
id parserResult = [self parseJSONResponse:JSONResponse];
completionHandler(parserResult, nil);
}
}];
This way you'll know when the network request is complete, and you can set the data you want on a property within your class. Then you could call tableView.reloadData in order to use the data in your table.
All that code would go into a service type class. I like to organize my services by responsibility. I don't know how many different data calls you make, but we have several for our project. If for instance you were making a weather app you could potentially organize by Current Conditions, Daily Forecasts, and Hourly Forecasts. I would make a service for each one of these requests. Say I created a CurrentConditionsService. The header would look something like this:
typedef void(^CurrentConditionsCompletionHandler)(CurrentConditions *currentConditions, NSError *error);
#interface CurrentConditionsService : NSObject
// locationKey is some unique identifier for a city
+ (instancetype)serviceWithLocationKey:(NSString *)locationKey;
- (void)retrieveCurrentConditionsWithCompletionHandler:(CurrentConditionsCompletionHandler)completionHandler;
#end
Then in my implementation file I would make the request and invoke the given completion handler like I demonstrated above. This pattern can be followed by many different services to the point where all your services could inherit from a base class that handles the request/response portions. Then your subclasses could override specific methods and handle/parse the data appropriately based on type.
If you go the route of parsing the JSON responses into model objects, all your parsers will need to conform to a protocol. This way in your super class it doesn't matter what the concrete implementation of your parser is. You supply the super class with a concrete implementation and all it knows how to do is invoke the parser and return the response.
An example JSON parser protocol would look like this:
#protocol AWDataParser <NSObject>
#required
- (id)parseFromDictionary:(NSDictionary *)dictionary;
- (NSArray *)parseFromArray:(NSArray *)array;
#end
And invoking it in your services super class:
- (id)parseJSONResponse:(id)JSONResponse error:(NSError **)error {
NSAssert(self.expectedJSONResponseClass != nil, #"parseResponse: expectedJSONResponseClass cannot be nil");
NSAssert(self.parser != nil, #"parseResponse: parser cannot be nil");
id parserResult = nil;
if (![JSONResponse isKindOfClass:self.expectedJSONResponseClass]) {
//handle invalid JSON reponse object
if (error) {
*error = [NSError errorWithDomain:NetworkServiceErrorDomain code:kNetworkServiceErrorParsingFailure userInfo:#{#"Invalid JSON type": [NSString stringWithFormat:#"expected: %#, is: %#",self.expectedJSONResponseClass, [JSONResponse class]]}];
}
} else {
if (self.expectedJSONResponseClass == [NSArray class]) {
parserResult = [self.parser parseFromArray:JSONResponse];
}else {
parserResult = [self.parser parseFromDictionary:JSONResponse];
}
if (!parserResult) {
if (error) {
*error = [NSError errorWithDomain:NetworkServiceErrorDomain code:kNetworkServiceErrorParsingFailure userInfo:nil];
}
}
}
return parserResult;
}
Use this approach:
NSURL *COMBINAT = [[NSURL alloc] initWithString:#"http://someapi.com/hello.json"];
dispatch_async(kBgQueue, ^{
NSData* data = [NSData dataWithContentsOfURL:
COMBINAT];
[self performSelectorOnMainThread:#selector(savedata:) withObject:data waitUntilDone:YES];
});
then simply call:
- (void)savedata:(NSData *)responseData {
NSError* error;
NSLog(#"Answer from server %#", responseData);
// ... your code to use responseData
}
Just create a property:
#property(nonatomic, strong) id savedResponseObject;
and set it in the success handler of the request:
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:#"http://someapi.com/hello.json"
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
self.savedResponseObject = responseObject;
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"Error: %#", error);
}];
Then you will be able to access it from other places in your class by referencing:
self.savedResponseObject