Does AFNetworking for iOS offer a solution to cache failed requests (e.g due to no internet connection) and automatically retry the request when the internet connection is back.
Thanks,
Dorin
See the Network Reachability Manager section of the AFNetworking site. By using "Reachability", your handler will be called whenever the network availability changes. Just set the setReachabilityStatusChangeBlock of the AFHTTPRequestOperationManager (AFNetworking 2) or AFHTTPSessionManager (AFNetworking 3):
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL];
[manager.reachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
switch (status) {
case AFNetworkReachabilityStatusReachableViaWWAN:
case AFNetworkReachabilityStatusReachableViaWiFi:
// do whatever you want when network is available
break;
case AFNetworkReachabilityStatusNotReachable:
default:
// do whatever you want when network is not available
break;
}
}];
[manager.reachabilityManager startMonitoring];
As matt said in AFNetworking issue #393,AFNetworking doesn't have retry mechanism:
This is something that a few people have requested, but each each use case had surprisingly different requirements in what the behavior should be, which leads me to believe that a general solution that's useful for all relevant cases is intractable.
I'm of the opinion that request retrying is an application concern (or perhaps even something for the user to initiate); it's not all that difficult to implement yourself:
- (void)downloadFileRetryingNumberOfTimes:(NSUInteger)ntimes
success:(void (^)(id responseObject))success
failure:(void (^)(NSError *error))failure
{
if (ntimes <= 0) {
if (failure) {
NSError *error = ...;
failure(error);
}
} else {
[self getPath:#"/path/to/file" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (success) {
success(...);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self downloadFileRetryingNumberOfTimes:ntimes - 1 success:success failure:failure];
}];
}
}
Nope, but you can detect in the failure block if the request has timeout. And you can resend it in this case (With like a retry counter of something like this).
Related
I'm writing a small iOS client for a server protected with OAuth2.
I'm wondering if is it possible using AFOAuth2Manager [here] auto-refreshing the expired token.
The idea is that the logic for refreshing the client when the server responds with a 401, or raise an error when the refresh method returns a 401 should be quite common, so probably it is integrated in some library.
I created a subclass of AFOAuth2Manager
In this subclass I override this method:
- (AFHTTPRequestOperation *)HTTPRequestOperationWithRequest:(NSURLRequest *)request
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure {
return [self HTTPRequestOperationWithRequest:request
success:success
failure:failure
checkIfTokenIsExpired:YES];
}
calling a custom method with an additional parameter: checkIfTokenIsExpired. This is required in order to avoid infinite loops.
The implementation of this method is straigth forward: if we don't need to check the token just call the super class.
if (!checkIfTokenIsExpired) {
return [super HTTPRequestOperationWithRequest:request
success:success
failure:failure];
}
otherwise we perform the request with a custom failure block
else {
return [super HTTPRequestOperationWithRequest:request
success:success
failure: ^(AFHTTPRequestOperation *operation, NSError *error) {
if (operation.response.statusCode == ERROR_CODE_UNAUTHORIZED) { //1
[self reauthorizeWithSuccess: ^{ //2
NSURLRequest *req = [self.requestSerializer requestByAddingHeadersToRequest:request]; //3
AFHTTPRequestOperation *moperation = [self HTTPRequestOperationWithRequest:req //4
success:success
failure:failure
checkIfTokenIsExpired:NO];
[self.operationQueue addOperation:moperation]; //5
} failure: ^(NSError *error) {
failure(nil, error);
}];
}
else {
failure(operation, error); //6
}
}];
}
//1: check the http status code, if 401 try to automatically re-authorize.
//2: reauthorize is a private mathod that uses AFOAuthManager to refresh the token.
//3: In this case we are re-authorized with success and we want to resubmit a copy of the previous request. The method requestByAddingHeadersToRequest: just copy all the header fields from the previous request.
//4: Create a copy of the previous request, but this time the last parameter is false because we don't want check again! The successBlock and failureBlock are the same of the previous request.
//5: Add the operation to the queue.
//6: If the reauthorize method fails just call the failure block.
Unfortunately I didn't found any framework for solve this problem so I wrote a short wrapper around AFNetworking (if someone is interested I can publish on github)
The logic is to execute the request, and in case of http response 401, try to refresh the auth-token and when it's done to re-execute the previous request.
I was searching an answer for this problem and "Matt", the creator of AFNetworking, suggest this:
the best solution I've found for dealing with this is to use dependent
NSOperations to check for a valid, un-expired token before any
outgoing request is allowed to go through. At that point, it's up to
the developer to determine the best course of action for refreshing
the token, or acquiring a new one in the first place.
Simple, but effective?, trying now, will edit with report...
Swift solution with Alamofire 4.0. Based on RequestAdapter and RequestRetrier protocols: example link
- (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.
I have a big array of dictionaries with the data of trips. I want to send it in all one request but I want to get responses after each dictionary send. I using afnetworking 2.0 and use AFHTTPRequestSerializer serializer.
Update. I use this method now, but if user get a lot of data there is a risk that it can be interrupted in case of bad internet. And i have to know what trips was been sent
- (void)sendTripData:(NSArray*)trips
withSuccessBlock:(RequestSuccessBlockWithDict)successBlock
failureBlock:(FailureBlock)failureBlock
{
NSString* path = #"add_trips";
NSDictionary *params = #{#"trips":trips,
#"mobile_id":[[UIDevice currentDevice].identifierForVendor UUIDString]};
[self POST:path
parameters:params
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSLogLight(#"Full data trips send success");
if ([responseObject isKindOfClass:[NSDictionary class]])
{
NSDictionary* dict = (NSDictionary*) responseObject;
if ([dict[#"status"] isEqualToString:#"ok"])
{
successBlock(responseObject);
}
else
{
failureBlock(nil);
}
}
else
{
failureBlock(nil);
}
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLogLight(#"Full data trips send failed");
NSString* errorReason = nil;
if (operation.response)
{
errorReason = error.localizedRecoverySuggestion;
}
failureBlock(errorReason);
}];
}
You should look into HTTP Pipelining. This isn't something that AFNetworking is necessarily going to help you with. The 'standard' model for HTTP requests is 1 request in, 1 response out. Optimizations like Keep-Alive and Pipelining exist, but will take more work to really take advantage of.
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