Stopping Asynchronous Block Request when Leaving View (AFNetworking; iOS) - ios

I am using AFNetworking (2.3.1) to parse JSON data and display it in labels.
To do this, I am using setCompletionBlockWithSuccess which is declared in AFHTTPRequestOperation.h.
Three functions like this are being called on viewDidLoad, one looks like:
-(void)parse {
NSURL *url = [[NSURL alloc] initWithString:kURL];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Parse Successful");
//Code for JSON Parameters and to display data
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"%#", [error localizedDescription]);
//Code for Failure Handling
}];
[operation start];
}
While this works like a charm, because it is being contained in a block request, this process continues throughout the application state. So when this data does not need to be displayed, the requests are still loading, and I am receiving memory warnings because of these blocks.
My question is, how can I stop, cancel, or pause these processes once I leave the View Controller that they are created on in order to save memory and data, or handle them appropriately?
Forgive me if this is an obvious answer, and I am just handling or creating blocks in a totally wrong way. I am new to both AFNetworking and Blocks, operations, async requests, and the like.
Thanks.

Assuming you have an AFHTTPRequestOperation object called operation:
[operation cancel];
This probably belongs in ViewWillDisappear.
And then inside your failure block (which will then be called) you can check to see if it failed because of an error or if you canceled it:
if ([operation isCancelled])
{
//I canceled it.
}
Update - a more concrete example of how to save a reference to the operation and cancel it when the view dissapears.
#interface myViewController ()
#property (strong, nonatomic) AFHTTPRequestOperation *parseOperation;
#end
#implementation myViewController
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
//no need to check to see if the operation is nil (because it never happened or it's complete) because
//messages sent to nil are ok.
[self.parseOperation cancel];
}
-(void)parse {
NSURL *url = [[NSURL alloc] initWithString:kURL];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
//save the parse operation so we can cancel it later on if we need to
self.parseOperation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
self.parseOperation.responseSerializer = [AFJSONResponseSerializer serializer];
__weak myViewController *weakSelf = self;
[self.parseOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
//nil out the operation we saved earlier because now that it's finished we don't need to cancel it anymore
weakSelf.parseOperation = nil;
NSLog(#"Parse Successful");
//Code for JSON Parameters and to display data
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (!operation.isCancelled) {
NSLog(#"%#", [error localizedDescription]);
//Code for Failure Handling
}
}];
[self.parseOperation start];
}

As it turns out, the lack of stopping was stemming from a timer (that called the block request, or the function parse) that was not invalidated once the view disappears. Invalidating the timer on -(void)viewDidDisappear: fixed the problem.

Related

ios - AFNetworking blocking main thread during download

I'm using AFNetworking to download more or less 200 images. The problem is that the main thread is blocked during the download, not during the success/failure block.
Here is my code:
imageDownloads=[[NSMutableArray alloc]init];
for(NSString *url in liens){
NSString *totalURL = [NSString stringWithFormat:#"http://%#", url];
[imageDownloads addObject:[[ImageDownload alloc] initWithURL:[NSURL URLWithString:totalURL] filename:nil]];
}
for (int i=0; i < imageDownloads.count; i++)
{
ImageDownload *imageDownload = imageDownloads[i];
[self downloadImageFromURL:imageDownload];
}
- (void)downloadImageFromURL:(ImageDownload *)imageDownload
{
NSURLRequest *request = [NSURLRequest requestWithURL:imageDownload.url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
imageDownload.totalBytesRead = totalBytesRead;
imageDownload.totalBytesExpected = totalBytesExpectedToRead;
[self updateProgressView];
}];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSAssert([responseObject isKindOfClass:[NSData class]], #"expected NSData");
imageDownload.totalBytesExpected = imageDownload.totalBytesRead;
[self updateProgressView];
//all kind of basic stuff here I left out: I get store the data inside CoreData
NSLog(#"finished %#", imageDownload);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"error %#", error);
}];
[operation start];
}
Basically, when I launch the code, the thread is blocked for like 30-40 seconds (the pictures are about 100MB in total), and then suddenly I can see all the NSLog logs appear with the "Finished"... text. So that part if really quick. But I thought AFNetworking wasn't supposed to block the main thread while I was downloading? This also doesn't allow me to track the progress of the download...Am I doing something wrong or misinterpreting something?
You're updating the progress view in the progress block. Because AFNetworking is inherently async anyway, each of these requests will stack and run at the same time. If you're running 200 of them, that's going to freeze up the app. Try using NSOperationQueue's maxConcurrentOperationCount to limit the number of concurrent threads.
Alternatively, you could save all the trouble and just use sdwebimage.

AFNetworking callbacks - delegate or notification?

I have a question on which is best way or the correct way to send AFNetworking results to controller. Is it via delegate or notification?
I created a class to handle make API calls that has the code below. So if imported this class to another controller and call this method to make API call. Should I do delegate or notification?
I have read www.raywenderlich.com/59255/afnetworking-2-0-tutorial and it is using delegates. I also been watched CodeSchool tutorial, which they used notification from Model to Controller.
I added the code below in a hope to better show my question.
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL];
// notification way inside the BLOCK
[ manager GET:path parameters:params
success:^(NSURLSessionDataTask *operation, id responseObject) {
[ [NSNotificationCenter defaultCenter] postNotificationName:notificationName
object:nil
userInfo:responseObject ];
} failure:^(NSURLSessionDataTask *operation, NSError *error) {
[ [NSNotificationCenter defaultCenter] postNotificationName:notificationName
object:nil ];
}];
// delegate way inside the BLOCK
[ manager GET:path parameters:params
success:^(NSURLSessionDataTask *operation, id responseObject) {
if ([delegate respondsToSelector:#selector(getUserFeedsDidFinish:resultDict:)])
{
[delegate performSelector:#selector(getUserFeedsDidFinish:resultDict:) withObject:self withObject:resultDict];
}
} failure:^(NSURLSessionDataTask *operation, NSError *error) {
if ([delegate respondsToSelector:#selector(getUserFeeds:didFailWithResultDict:)]) {
[delegate performSelector:#selector(getUserFeeds:didFailWithResultDict:)
withObject:self
withObject:[NSDictionary dictionaryWithObject:error.userInfo forKey:KEY_ERRORS]];
}
}];
I will recommend use blocks, how? I will write a service for you, this one is wrote in a class called Connection:
+(void)requestLocation:(NSString*)googleReference completionBlock:(void (^)(NSString * coordinates, NSError * error)) handler{
NSString * urlString = #"https://maps.googleapis.com/maps/";
NSMutableDictionary * parametersDictionary = [NSMutableDictionary dictionary];
[parametersDictionary setObject:googleReference forKey:#"reference"];
[parametersDictionary setObject:#"true" forKey:#"sensor"];
[parametersDictionary setObject:#"key(it is not)" forKey:#"key"];
AFHTTPClient *HTTPClient = [AFHTTPClient clientWithBaseURL:[NSURL URLWithString:urlString]];
NSURLRequest *URLRequest = [HTTPClient requestWithMethod:#"GET" path:#"api/place/details/json" parameters:parametersDictionary];
AFHTTPRequestOperation *requestOperation = [[AFHTTPRequestOperation alloc] initWithRequest:URLRequest];
[requestOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSError * error = nil;
NSDictionary * response = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableContainers error:&error];
NSDictionary * dicGeo = [((NSDictionary*)[response objectForKey:#"result"]) objectForKey:#"geometry"];
NSDictionary * coords = [dicGeo objectForKey:#"location"];
NSNumber * lat = [coords objectForKey:#"lat"];
NSNumber * lng = [coords objectForKey:#"lng"];
NSString * coordinates = [NSString stringWithFormat:#"%#,%#", lat.description, lng.description];
handler(coordinates, error);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"%#", error);
}];
[requestOperation start];
}
Then to call this service:
[Connection requestLocation:#"google reference (it is not)" completionBlock:^(NSString *coordinates, NSError *error) {
//Your code with results.
}
I've only scratched the surface with AFNetworking. From what I've seen, most of it seems to use a third approach, blocks.
Blocks are somewhat new, and different than both delegates and notifications.
Blocks are an extension to C function pointers that let you pass code into a method when you call it.
A common design pattern using blocks is to create a method that takes a completion block. A completion block is a piece of code that gets invoked when an async request is completed.
Take the AFNewtworking method HTTPRequestOperationWithRequest as an example. That method takes a success block, that gets called if the request succeeds, and a failure block, that gets called if the request fails.
Block is the easiest way to use IMO. You don't need to implement extra delegate methods or you don't need any conformations.
Basically define your wrapper like this.
typedef void(^SampleRequestCompletion)(NSError *error, id data);
- (void)GET:(NSString *)URLString
parameters:(NSDictionary *)parameters
completion:(SampleRequestCompletion)completion
{
[self GET:URLString parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
// Do what you want
if (completion) {
completion(nil, data);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Failure case
if (completion) {
completion(error,nil);
}
}];
}
And call this method from any objects like this,
[self GET:path parameters:dictionary completion:^(NSError *error, id data) {
}];
So you can manage what to do whenever the call ends with success or failure.
As the tutorial recommended, we can extract the web service related code into a module which acts more like a model level thing. Considering the communication between the network module and views, view invoke/start the request on a singleton web service client, once response back the usual workflow would be send the result to view controller and show the data in the views. We don't need to return anything back to network module.
So this workflow is more like a notification than delegation. And set the V as the M's delegate, it's weird.
Notification : Hey, man, I have done my job, it's your turn.
Delegation: Hey, man, I have done lots, now I need you cover/back up/provide me some tasks, then I will continue/complete the work.
In some situations, it's difficult to choose which one better. For AFNetworking, I thought the Notification approach better.

AFNetworking 2.0 implementation code not working

I am trying to implement AFNetworking code to communicate with a web API. I am getting the following errors in the code:
No visible #interface for APIClass declares the selector
registerHTTPOperationClass
No visible #interface for APIClass declares the selector
setDefaultHeader:Value
No visible #interface for APIClass declares the selector
multiPartFormRequestWithMethod:path:parameters:constructingBodyWithblock
Obviously something to do with the new AFNetworking 2.0 migration...however I have been looking at all the migration posts and documentation and connot find the replacements for these without throwing an error:
// add the location details of the web service we wrote
#define kAPIHost #"http://myurl"
#define kAPIPath #"mywebapi/"
#implementation APIClass
// this is the implementation of the singleton method
+(APIClass*)sharedInstance{
static APIClass *sharedInstance = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
sharedInstance = [[self alloc] initWithBaseURL:[NSURL URLWithString:kAPIHost]];
});
return sharedInstance;
}
-(APIClass*)init{
// call super init
self = [super init];
if (self != nil){
user = nil;
[self registerHTTPOperationClass:[AFHTTPRequestOperation class]];
// Accept HTTP header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
[self setDefaultHeader:#"Accept" value:#"application/json"];
}
return self;
}
// call to the server
-(void)commandWithParams:(NSMutableDictionary*)params onCompletion:
(JSONResponseBlock)completionBlock
{
// prepare e POST request by creating an NSMutableURLRequest instance using the
// parameters we want to send via POST
NSMutableURLRequest *apiRequest =
[self multipartFormRequestWithMethod:#"POST"
path:kAPIPath
parameters: params
constructingBodyWithBlock: ^(id <AFMultipartFormData>formData) {
// attach file if needed
}];
// create an operation to handle the network communication in the background
// and intialize it with the POST request we just prepared
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:
apiRequest];
// now set the 2 blocks needed for success and failure
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id
responseObject)
{
// success! - if the call is successful then we just pass in the JSON response
NSLog(#"responseObject: %#", responseObject);
completionBlock(responseObject);
}
// if there is a failure in the network call then we call the failure block
// and contrcut a new dictinary to hold the message of the network error
failure:^(AFHTTPRequestOperation *operation, NSError * error) {
//failure!
completionBlock([NSDictionary dictionaryWithObject:[error localizedDescription]forKey:#"error"]);
}];
// at this point we can call the start method so that AFNetworking can do its
// magic in the background
[operation start];
}
#end
You're getting these errors because the methods you're calling aren't methods of whatever class you're subclassing. I'll assume you're subclassing AFHTTPSessionManager, which is recommended for iOS 7 in AFNetworking 2.0. Based on that...
For your first two errors, I believe the updated lines below are the AFNetworking 2.0 way of doing it with AFHTTPSessionManager:
-(APIClass*)init{
// call super init
self = [super init];
if (self != nil){
user = nil;
self.requestSerializer = [AFJSONRequestSerializer serializer];
self.responseSerializer = [AFJSONResponseSerializer serializer];
}
}
For your third error, the method multiPartFormRequestWithMethod:path:parameters:constructingBodyWithblock should be replaced with:
[self POST:kAPIPath parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
// attach file if needed
} success:^(NSURLSessionDataTask *task, id responseObject) {
// handle success
} failure:^(NSURLSessionDataTask *task, NSError *error) {
// handle failure
}];

Multiple AFHTTPClient Request

I've got subclass of AFHTTPClient
The main idea is that i call all API through my singleton of AFHTTPClient subclass, and all requests goes through 1 points for error handling and HUD displaying.
This is entry point for every API calls:
-(void) makeRequestWithPath:(NSString*) path andParams:(NSDictionary*) params
success:(void (^)( id JSON, AFHTTPRequestOperation *operation)) success
failure:(void (^)( NSError *error)) failure
And i've got many methods for API calls something like that:
-(void) getListMainTreeWithSuccess:(void (^)( id JSON, AFHTTPRequestOperation *operation)) success
failure:(void (^)( NSError *error)) failure
{
[self makeRequestWithPath:#"objects/selectlist" andParams:nil success:^(id JSON, AFHTTPRequestOperation *operation) {
success(JSON,operation);
} failure:^(NSError *error) {
failure(error);
}];
}
This works just fine for my needs. But i faced problem that i need to make serial request in loop through my AFHTTPClient subclass and make some action when all of them are finished , I found method
-(void)enqueueBatchOfHTTPRequestOperationsWithRequests:(NSArray *)urlRequests
progressBlock:(void (^)(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations))progressBlock
completionBlock:(void (^)(NSArray *operations))completionBlock
which should solve my issue, but the problem is that i call all methods through AFHTTPClient and it's methods getPath: and postPath: and previous way forces me to rewrite everything and makes my subclass completely useless, because I need to add there NSArray of AFHTTPRequestoperation, which is not possible to construct or extract from my subclass and my methods. Previously i tried to use __block 's to synchronise requests with semaphore and something else but i failed to get what i need, please help me!
UPDATE:
It seems that it is not possible to even use enqueueBatchOfHTTPRequestOperations method (even with rewriting all my code) because this method needs array of http request operations, but it's not possible to construct POST request with them.
I solved this with an increment/decrement pending download system and tied the HUD to that.
[networkStatus beginNetworkActivity];
[client someRESTActionWithCompletion:^(id object, NSError *error) {
[networkStatus endNetworkActivity];
if (error) {
// Handle the error ...
}
if (![networkStatus hasNetworkActivity]) {
// All downloads have finished
}
}];
I keep the network status object separate which from the AFHTTPClient subclass, but it can be built into the client if that's what you want.
Network status keeps an internal counter. -beginNetworkActivity increments the counter, if the counter was 0, then it displays a HUD. -endNetworkActivity decrements the counter, if the counter becomes 0, then it dismisses the HUD. -hasNetworkActivity returns YES if the counter greater than 0.
Other Notes: I combine the success and failed callbacks into a single completion callback. I keep the network status logic separate from the client because sometime I'll use a singleton network status object, sometimes I'll use a created instance, sometimes I won't use one at all. It all depends on the needs to the higher level logic.
Again, as #MikePollard said, create AFHTTPRequestOperation using
[AFHHTPClient HTTPRequestOperationWithRequest:success:failure:]
For this method create NSURLRequest using (or use another one, pick which one is suitable for you). Here you can also specify, which method to use POST, GET or any other.
[AFHTTPClient requestWithMethod:
path:
parameters:]
After that save all operation to an NSArray, and schedule them using:
[AFHTTPClient enqueueBatchOfHTTPRequestOperationsWithRequests:
progressBlock:
completionBlock:]
Code example:
NSMutableArray *ops = [NSMutableArray new];
NSMutableURLRequest *request1 = [[AFHTTPClient sharedClient] requestWithMethod:#"GET"
path:#"MyEndpoint"
parameters:#{#"key1": #"value"}];
AFHTTPRequestOperation *op1 = [[AFHTTPClient sharedClient] HTTPRequestOperationWithRequest:request1
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Success!");
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Failure!");
}];
[ops addObject:op1];
NSMutableURLRequest *request2 = [[AFHTTPClient sharedClient] requestWithMethod:#"POST"
path:#"MyAnotherEndpoint"
parameters:#{#"key2": #(104)}];
AFHTTPRequestOperation *op2 = [[AFHTTPClient sharedClient] HTTPRequestOperationWithRequest:request2
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Success!");
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Failure!");
}];
[ops addObject:op2];
[[AFHTTPClient sharedClient] enqueueBatchOfHTTPRequestOperationsWithRequests:ops
progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
NSLog(#"numberOfFinishedOperations: %d totalNumberOfOperations %d",
numberOfFinishedOperations,
totalNumberOfOperations);
}
completionBlock:^(NSArray *operations) {
NSLog(#"All operation compelted!");
}];

How to wait for all asynchronously tasks?

I download asynchronously some object, I store it in array. Next for each object I download some coordinates with geocoding (it is also asynchronously), and update my database for each object with new parameters which is coordinate. My method looks like this:
- (void)downloadObjectsWithTitle:(NSString *)title andHandler:(void(^)(NSMutableDictionary *result))handler {
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET"
path:nil
parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[httpClient registerHTTPOperationClass:[AFHTTPRequestOperation class]];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
//I get here array of objects
//now for each object I want to download geocoding localization so i called another asynchronyous method getLocationWithTitle:andHandler;
for(int i = 0; i < resutArray.count; i++) {
[self downloadLocationWithString:[dictionary objectForKey:#"string"] andHandler:^(NSMutableDictionary *result) {
//update database;
}];
}
handler(dictionary);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
[operation start];
}
My question is how to downalod coordinates for each object and that fire:
handler(dictionary);
so wait for each coordinates download (for each object) before quit method (fire handler).
Thnaks for all sugestions.
Maintain a count of all the tasks. When it's zero you're done.
Assuming you're using dispatch_async in downloadLocationWithString: on a concurrent queue:
dispatch_barrier_async(queue, ^{
// will only be called after all the blocks submitted to queue have finished.
}];
(If you're using serial queue, simply call handler at the last line of the last block)
Try a global flag. set NO first. In download block, after download complete set flag to yes. You can check that flag.

Resources