I am trying out afnetworking 2.0 and just trying to figure out how to cancel specific tasks.
The old way would be to use something like
[self cancelAllHTTPOperationsWithMethod:#"POST" path:#"user/receipts"]
but I dont see anything like this in 2.0
I created a sub class of AFHTTPSessionManager which gives me access to the array of pending tasks and I can cancel them directly but I dont know how to identify 1 task from another so I can cancel only specific tasks.
Task does have an taskidentifier but this doesnt appear to be what I need.
NSString *path = [NSString stringWithFormat:#"user/receipts"];
[self.requestSerializer setAuthorizationHeaderFieldWithUsername:[prefs valueForKey:#"uuid"] password:self.store.authToken];
[self GET:path parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
completionBlock(responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
errorBlock(error);
}];
now if i wanted to cancel this request only how would I approach this?
You can store the task in a variable so you can access it later:
NSURLSessionDataTask* task = [self GET:path parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
completionBlock(responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
errorBlock(error);
}];
Then simply cancel it with [task cancel].
Another way would be to save the task ID of the task and later ask the URL session for its tasks and identify the task you wish to cancel:
// save task ID
_savedTaskID = task.taskIdentifier;
// cancel specific task
for (NSURLSessionDataTask* task in [self dataTasks]) {
if (task.taskIdentifier == _savedTaskID) {
[task cancel];
}
}
No need to save it, here is my implementation, use your subclass of AFURLSessionManager for cancelling specific request:
- (void)cancelAllHTTPOperationsWithPath:(NSString *)path
{
AFURLSessionManager * yourSessionManager = [self getSessionManager];
[[yourSessionManager session] getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
[self cancelTasksInArray:dataTasks withPath:path];
[self cancelTasksInArray:uploadTasks withPath:path];
[self cancelTasksInArray:downloadTasks withPath:path];
}];
}
- (void)cancelTasksInArray:(NSArray *)tasksArray withPath:(NSString *)path
{
for (NSURLSessionTask *task in tasksArray) {
NSRange range = [[[[task currentRequest]URL] absoluteString] rangeOfString:path];
if (range.location != NSNotFound) {
[task cancel];
}
}
}
you can do the following
NSArray *operations = [[[MyClient sharedClient] operationQueue] operations];
if(operations && operations.count > 0){
for (NSOperation *operation in operations) {
if([operation isKindOfClass:[AFHTTPRequestOperation class]]){
AFHTTPRequestOperation *httpOperation = (AFHTTPRequestOperation *)operation;
NSLog(#"%#", [[httpOperation request] URL]);
//--- if this is your request then cancel it --> [httpOperation cancel];
}
}
}
Where MyClient is a child of AFHTTPClient and the function sharedClient is a static function which returns a singleton instance of MyClient
Related
Since I am new to IOS and AFNetworking 3,0 is new, I don't know how to retrieve data from AFHTTPSessionManager.
I have to following message and I want to return the result
- (NSString *) makeServiceCall;
{
NSString *response = #"";
#try {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager
POST:self.url.absoluteString
parameters:self.parameters
progress:nil
success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Success: %#", responseObject);}
failure:^(NSURLSessionDataTask * task, NSError * error) {
NSLog(#"Error: %#", error);
}];
[AFHTTPSessionManager manager].securityPolicy.allowInvalidCertificates = YES;
}
#catch (NSException *exception) {
NSLog(#"%#", exception.reason);
}
}
The method AFHTTPSessionManager POST:parameters:progress:success:failure: is an asynchronous method.
What you are trying to do is return a string from the method calling it. This will not work as the method will finish before the download has started.
You need to call this with a completion block something like this...
- (void)getStringWithCompletionHandler:(void (^)(id))completion {
NSLog(#"Method started");
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager
POST:self.url.absoluteString
parameters:self.parameters
progress:^(NSProgress * _Nonnull uploadProgress) {
NSLog(#"Download underway");
}
success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Download successful");
completion(responseObject);
}
failure:^(NSURLSessionDataTask * task, NSError * error) {
NSLog(#"Error");
}];
// trying to return a string here won't work because the download hasn't finished yet.
// You can see the order of things happening by adding logs...
NSLog(#"Method finished");
}
The order of the logs in this code will be...
Method started
Method finished
Download underway
Download successful
As you can see, trying to return at the end of the method won't work because the download won't have completed yet.
I have a scenario where I need to quiet refresh auth token (relogin) again if it expired when I accessing other API but I'm having a hard time thinking how to code this without creating redundant codes for every APIs even though the flow is similar.
When user has expired auth token > call paid API A (return 401 unauthorised) > relogin again > call paid API A (run successfully)
I'm having difficult in wrapping my mind to call paid API A the second time with less code and not falling into infinite loop trap. Is there any method useful for this case like NSNotification center?
Note: I need to use API in this format from AFNetworkinglogin
- (NSURLSessionDataTask *)getApiA:(CallbackBlock)block{
CallbackBlock _block = [block copy];
NSString *urlString = [[NSURL URLWithString:GET_API_A_URL relativeToURL:[NSURL URLWithString:HOME_URL]] absoluteString];
return [self GET:urlString parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
NSDictionary *response = (NSDictionary *)responseObject;
BLOCK_SAFE_RUN(block, response, nil, task);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
if([self unauthorizedAccess:task]){ //401
***//call Login once again > run getApiA again***
}else if ([self forbiddenAccess:task]){ //403
}
BLOCK_SAFE_RUN(block, nil, error, task);
}];
}
If i get it right you could split it into 2 methods. And pass a bool for trying again. e.g.:
- (NSURLSessionDataTask *)getApiA:(id)block {
NSString *urlString = [[NSURL URLWithString:GET_API_A_URL relativeToURL:[NSURL URLWithString:HOME_URL]] absoluteString];
return [self doApiACallWithURL:urlString firstTry:YES completion:block];
}
- (NSURLSessionDataTask *)doApiACallWithURL:(NSString *)url firstTry:(BOOL)first completion:(CallbackBlock)completion {
__weak typeof(self) wself = self;
return [self GET:urlString parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
NSDictionary *response = (NSDictionary *)responseObject;
BLOCK_SAFE_RUN(block, response, nil, task);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
if ([wself unauthorizedAccess:task]) { //401
if (first) {
[wself doApiACallWithURL:url firstTry:NO completion:completion];
}
} else if ([wself forbiddenAccess:task]) { //403
}
BLOCK_SAFE_RUN(block, nil, error, task);
}];
}
and use a weak self for blocks is in most cases a good idea.
I have this bool method that returns a yes or no for an inputted string.
I'm successfully able to return a YES or a NO, but I cannot seem to able to make a network connection and return a YES or a NO depending on the server's response.
I tried using __block and I don't feel like that will wait for the web request to finish, is there a way to return YES or NO in the success block without it giving me the error:
Incompatible block pointer types sending 'BOOL(^)(NSURLSessionTask*__strong, NSError __strong' to parameter of the type 'void(^)(NSURLSessionTask...)
-(BOOL)customResponseForString:(NSString *)text {
__block BOOL response_available;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
[manager.responseSerializer setAcceptableContentTypes:[NSSet setWithObject:#"text/plain"]];
[manager GET:[NSString stringWithFormat:#"http://example.com/response.php?input=%#", text] parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
NSDictionary *response = [NSJSONSerialization JSONObjectWithData:responseObject options:NSUTF8StringEncoding error:nil];
response_available = (BOOL)response[#"response_available"];
if (response_available) {
[session sendTextSnippet:response[#"response"] temporary:NO scrollToTop:NO dialogPhase:#"Summary"];
} else {
response_available = NO;
}
[session sendTextSnippet:[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding] temporary:NO scrollToTop:NO dialogPhase:#"Summary"];
[session sendRequestCompleted];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
//return NO;
}];
});
return response_available;
}
Your block definition syntax is probably erroneous, because you can definitely return a BOOL along other parameters in a block.
- (void)fetchCurrentUserWithCompletion:(void (^)(BOOL success, User *user))completion;
This method would be called like this:
[self.userProfileController fetchCurrentUserWithCompletion:^(BOOL success, User *user) {
if (success) {
NSLog(#"Current User Name: %#", user.fullName);
}
}];
If you use AFNetworking, check the AFHTTPRequestOperation object that handle completionBlocks:
[requestOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
User *user = [self userFromResponseObject:responseObject];
if (completion) completion(YES, user);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (completion) completion(NO, user);
}];
Because you are implicitly initializing response_available to NO and then using an async GCD call, your method as written will always immediately return NO without waiting for the request to finish. Note: switching to dispatch_sync won't help either because AFNetworking will queue the GET request asynchronously either way.
Best Approach
Add a completion block argument to customResponseForString:. Then simply execute your completion block in the success or failure blocks of the AFHTTPRequestOperation.
Workable Approach (use caution!)
It is possible to make customResponseForString: wait for a response to the network request, but you will have significant issues if it is ever called from the main thread.
First you create a dispatch group and tell it you are starting some long-running work:
dispatch_group_t networkGroup = dispatch_group_create();
dispatch_group_enter(networkGroup);
Then you need to make your network request and when it completes tell the group that the work is finished with dispatch_group_leave():
[manager GET:[NSString stringWithFormat:#"http://example.com/response.php?input=%#", text] parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
NSDictionary *response = [NSJSONSerialization JSONObjectWithData:responseObject options:NSUTF8StringEncoding error:nil];
response_available = (BOOL)response[#"response_available"];
if (response_available) {
[session sendTextSnippet:response[#"response"] temporary:NO scrollToTop:NO dialogPhase:#"Summary"];
} else {
response_available = NO;
}
[session sendTextSnippet:[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding] temporary:NO scrollToTop:NO dialogPhase:#"Summary"];
[session sendRequestCompleted];
dispatch_group_leave(networkGroup);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
response_available = NO;
dispatch_group_leave(networkGroup);
}];
Before your original method returns, tell it to wait for the entire group to finish processing:
dispatch_group_wait(networkGroup, DISPATCH_TIME_FOREVER);
return response_available;
You could adjust this time interval as needed or leave it at DISPATCH_TIME_FOREVER to let the network request time out on its own.
On previous versions of AFNetworking I could make use of AFHTTPRequestOperation to create multiple requests, create dependencies between them and enqueue them pretty easily. Example (inside of an AFHTTPClient subclass):
NSURLRequest *categoriesRequest = [self requestWithMethod:#"GET" path:#"categories" parameters:nil];
AFHTTPRequestOperation *categoriesOperation = [self HTTPRequestOperationWithRequest:categoriesRequest success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSArray *jsonCategories = responseObject;
for (NSDictionary *jsonCategory in jsonCategories) {
SPOCategory *category = [[SPOCategory alloc] initWithDictionary:jsonCategory];
[self.categories addObject:category];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// …
}];
NSURLRequest *incidencesRequest = [self requestWithMethod:#"GET" path:#"incidences" parameters:nil];
AFHTTPRequestOperation *incidencesOperation = [self HTTPRequestOperationWithRequest:incidencesRequest success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSArray *jsonIncidences = responseObject;
for (NSDictionary *jsonIncidence in jsonIncidences) {
SPOIncidence *incidence = [[SPOIncidence alloc] initWithDictionary:jsonIncidence];
[self.incidences addObject:incidence];
}
completionBlock(self.incidences, self.categories, nil);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// …
}];
[incidencesOperation addDependency:categoriesOperation];
[self enqueueBatchOfHTTPRequestOperations:#[categoriesOperation, incidencesOperation] progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
// Processing…
} completionBlock:^(NSArray *operations) {
// Completed
}];
I know I can continue to make use of AFHTTPRequestOperation but, I'd like to know if there is a similar way to achieve the same thing inside a subclass of AFHTTPSessionManager, using NSURLSession as the backing library instead of NSURLConnection.
Thank you!
AFHTTPSessionManager's connection factory methods create connections which will be represented by a NSURLSessionDataTask object.
Unlike AFHTTPRequestOperation these are not NSOperation subclasses, and thus declaring dependencies is not possible.
One could imagine to wrap a factory method like
- (NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(NSDictionary *)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;
into a helper method/function which returns a NSOperation object. That might (will) become cumbersome and looks quite weird, though.
If you are courageous enough to consider another third party library, you can solve your problem as explained below:
The idea is to represent the eventual result of the asynchronous operation by a "Promise". Think of a Promise as a placeholder of the result, which will eventually be set by the operation. So, basically you wrap a factory method into one which then effectively yields a method having this signature:
-(Promise*) fetchCategories;
or
-(Promise*) fetchCategoriesWithParameters:(NSDictionary*)parameters;
Notice that above methods are asynchronous - yet they have no completion handler. The Promise will instead provide this facility.
Initially, when fetchCategories returns, the promise object does not "contain" the result.
You obtain (at some tme later) the eventual result respectively and error by "registering" a completion handler block respectively an error handler block with a then property like so (pseudo code):
[self.fetchCategoriesWithParameters].then(
<success handler block>,
<failure handler block> );
A more complete code snippet:
Promise* categoriesPromise = [self fetchCategories];
categoriesPromise.then(^id(id result){
self.categories = result;
... // (e.g, dispatch on main thread and reload table view)
return nil;
}, ^id(NSError* error){
NSLog(#"Error: %#", error);
return nil;
});
Note: The parameter result of the success handler block is the eventual result of the operation, aka the responseObject.
Now, in order to "chain" multiple asynchronous operations (including the handlers), you can do this:
self.categoriesPromise = [self fetchCategories];
Promise* finalResult = self.categoriesPromise.then(^id(id result){
NSArray *jsonCategories = result;
for (NSDictionary *jsonCategory in jsonCategories) {
SPOCategory *category = [[SPOCategory alloc] initWithDictionary:jsonCategory];
[self.categories addObject:category];
}
return [self fetchIncidencesWithParams:result);
}, nil)
.then(^id(id result){
NSArray *jsonIncidences = result;
for (NSDictionary *jsonIncidence in jsonIncidences) {
SPOIncidence *incidence =
[[SPOIncidence alloc] initWithDictionary:jsonIncidence];
[self.incidences addObject:incidence];
}
return #[self.incidences, self.categories];
}, nil)
.then(^id(id result){
NSArray* incidences = result[0];
NSArray* categories = result[1];
...
return nil;
}, nil /* error handler block */);
You create and "resolve" (that is, setting the result) a Promise as shown below:
- (Promise*) fetchCategories {
Promise* promise = [[Promise alloc] init];
NSURLRequest *categoriesRequest = [self requestWithMethod:#"GET" path:#"categories" parameters:nil];
AFHTTPRequestOperation *categoriesOperation = [self HTTPRequestOperationWithRequest:categoriesRequest success:^(AFHTTPRequestOperation *operation, id responseObject) {
[promise fulfillWithResult:responseObject];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[promise rejectWithReason:error];
}];
return promise;
}
Disclaimer:
There are a few third party Objective-C libraries which implement a Promise in this or a similar way. I'm the author of RXPromise which implements a promise according the Promises/A+ specification.
At this moment I have a method that calls for the download of data from the web using AFHTTPRequestOperation like so:
- (void)downloadDataForRegisteredObjects:(BOOL)useUpdatedAtDate {
NSLog(#"downloadDataForRegisteredObjects");
NSMutableArray *operations = [NSMutableArray array];
for (NSString *className in self.registeredClassesToSync) {
NSDate *mostRecentUpdatedDate = nil;
if (useUpdatedAtDate) {
mostRecentUpdatedDate = [self mostRecentUpdatedAtDateForEntityWithName:className];
}
NSMutableURLRequest *request = [[SDAFParseAPIClient sharedClient] GETRequestForAllRecordsOfClass:className updatedAfterDate:mostRecentUpdatedDate];
AFHTTPRequestOperation *operation = [[SDAFParseAPIClient sharedClient] HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
if ([responseObject isKindOfClass:[NSDictionary class]]) {
// Write JSON files to disk
[self writeJSONResponse:responseObject toDiskForClassWithName:className];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Request for class %# failed with error: %#", className, error);
[[NSNotificationCenter defaultCenter]
postNotificationName:kSDSyncEngineSyncINCompleteNotificationName
object:nil];
}];
[operations addObject:operation];
}
[[SDAFParseAPIClient sharedClient] enqueueBatchOfHTTPRequestOperations:operations progressBlock:^(NSUInteger numberOfCompletedOperations, NSUInteger totalNumberOfOperations) {
} completionBlock:^(NSArray *operations) {
// Process JSON into CD
if (useUpdatedAtDate) {
[self processJSONDataRecordsIntoCoreData];
}
}];
}
From what I understand, we create an NSURLMutableRequest, pass it to an AFHTTPRequestOperation with a success & failure block.
The success block says, if and when successful, test if dictionary and if so, write it to disk. The failure block says, log the error and post a notification.
The method gets called twice in my app, in series, one after the other. The first time it returns an empty responseObject but the second time it returns a full responseObject.
Why should that be the case?