How to return value in completion block - ios

I have class that manages connections with AFNetworking.
So I want to call my function like NSDictionary *dict = [ServerManager requestWithURL:#"https://someurl.com"];
And that's the function in the other class:
- (NSDictionary *) requestWithURL:(NSString *)requestURL {
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] init];
[manager GET:requestURL parameters:nil progress:nil
success:^(NSURLSessionDataTask *operation, id responseObject){
return responseObject;
}
failure:^(NSURLSessionDataTask *operation, NSError *error) {
}];
}
I know that is incorrect to do that. So what should I do to return responseObject back to NSDictionary *dict? I'd like to get the basic idea of asynchronous development with blocks.

Since the networking request completes long after its is launched, the only way to handle the result is with a block passed to your request method...
// when request completes, invoke the passed block with the result or an error
- (void)requestWithURL:(NSString *)requestURL completion:(void (^)(NSDictionary *, NSError *))completion {
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] init];
[manager GET:requestURL parameters:nil progress:nil success:^(NSURLSessionDataTask *operation, id responseObject){
if (completion) completion((NSDictionary*)responseObject, nil);
}, failure:^(NSURLSessionDataTask *operation, NSError *error) {
if (completion) completion(nil, error);
}];
}
Make it public in a ServerManager.h
- (void)requestWithURL:(NSString *)requestURL completion:(void (^)(NSDictionary *, NSError *))completion;
Elsewhere, call it:
[ServerManager requestWithURL:#"http://someurl.com" completion:^(NSDictionary *dictionary, NSError *error) {
// check error and use dictionary
}];

Related

AFNetworking - stuck after posting http data

I've inherited some Objective-C code from 2013: so old that it used AFNetworking 1.0!
#implementation AFClaimNotificationAPIClient
+ (AFClaimNotificationAPIClient *)sharedClient {
static AFClaimNotificationAPIClient *_sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedClient = [[AFClaimNotificationAPIClient alloc] initWithBaseURL:[NSURL URLWithString:kClaimNotificationURL]];
});
return _sharedClient;
}
- (void) submit:(ClaimNotification *) claimNotification
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure {
NSMutableURLRequest *request = [self multipartFormRequestWithMethod:#"POST" path:kClaimNotificationURL parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[self populateFormDataForJson:formData andClaimNotification:claimNotification];
[self populateFormDataWithAttachemnts:formData andClaimNotification:claimNotification];
}];
AFJSONRequestOperation *operation = [[AFJSONRequestOperation alloc] initWithRequest:request];
DDLogVerbose(#"%#", request.HTTPBody);
[operation setCompletionBlockWithSuccess:success failure:failure];
[operation start];
}
In the corresponding header file for this class, the AFClaimNotificationAPIClient is defined thus:
#interface AFClaimNotificationAPIClient : AFHTTPClient
and AFHTTPClient no longer exists. It was dropped in AFNetworking 2.0, which came out shortly after this code was written.
After much forum searching, I've actually managed to get it partially working again by upgrading to AFNetworking 2.0, and redefining AFClaimNotificationAPIClient as an AFHTTPSessionManager:
#interface AFClaimNotificationAPIClient : AFHTTPSessionManager
My submit button code now looks like this:
- (void) submit:(ClaimNotification *) claimNotification
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure {
NSURLSessionDataTask *request = [self POST:kClaimNotificationURL parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[self populateFormDataForJson:formData andClaimNotification:claimNotification];
[self populateFormDataWithAttachemnts:formData andClaimNotification:claimNotification];
} success:^(NSURLSessionDataTask *task, id responseObject) {
DDLogVerbose(#"Post success");
// handle success
} failure:^(NSURLSessionDataTask *task, NSError *error) {
DDLogVerbose(#"Post error");
}];
// [operation start];
[request resume]; // [request start] doesn't work
}
I chose AFHTTPSessionManager as the new type for my class because it's the only one that I could find that contains the constructingBodyWithBlock definition, and I was trying to keep the code as close to the original as possible.
Amazingly enough, my reworked code actually posts data to the server and gets a reply. However, the app hangs at that point because the calling code (not shown here) is not receiving a success (or failure) message. I can see that I've removed a whole step from the original code - the setting up of the operation variable and then the triggering of its setCompletionBlockWithSuccess method.
You need to execute the block according to the response passing a responseObject or an error with the corresponding operation
try this
- (void) submit:(ClaimNotification *) claimNotification
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure {
NSURLSessionDataTask *request = [self POST:kClaimNotificationURL parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[self populateFormDataForJson:formData andClaimNotification:claimNotification];
[self populateFormDataWithAttachemnts:formData andClaimNotification:claimNotification];
} success:^(NSURLSessionDataTask *task, id responseObject) {
DDLogVerbose(#"Post success");
// handle success
success(nil,responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
DDLogVerbose(#"Post error");
failure(nil, error);
}];
// [operation start];
[request resume]; // [request start] doesn't work
}
You can use AFHTTPRequestOperation as well and this will return both parameters needed in your callbacks

Untraceable AFNetworking memory leak

TL;DR : Clone and check leak yourself https://github.com/JakubMazur/SO41343532/
I have a one class that handle all my networking. It's called ResponseOrganizer and in there I have a class method:
+ (void)getSth:(void (^)(NSURLSessionDataTask *operation, NSArray *locales, id plainObject))success failure:(void (^)(NSURLSessionDataTask *operation, NSError *error))failure {
Connection *connection = [Connection new];
connection.urlString = #"http://sample-file.bazadanni.com/download/txt/json/sample.json";
connection.requestMethodType = GET;
[connection fireWithSuccess:^(NSURLSessionDataTask *operation, NSArray *returnArray, id originalResponse) {
success(operation, returnArray, originalResponse);
} failure:^(NSURLSessionDataTask *operation, NSError *error) {
failure(operation, error);
}];
}
Where Connection is a single my internal connection object:
Here is the implementation:
#import "Connection.h"
#interface Connection()
#property (weak,nonatomic) AFHTTPSessionManager *manager;
#end
#implementation Connection
#pragma mark - Connection groundwork
-(void)fireWithSuccess:(void (^)(NSURLSessionDataTask *operation, NSArray* returnArray, id originalResponse))success failure:(void (^)(NSURLSessionDataTask *operation, NSError *error))failure {
self.manager = [AFHTTPSessionManager manager];
[self.manager urlString:self.urlString withMethod:self.requestMethodType parameters:self.paramaters success:^(NSURLSessionDataTask *operation, id responseObject) {
success(operation,#[responseObject],nil);
} failure:^(NSURLSessionDataTask *operation, NSError *error) {
failure(operation,error);
}];
}
#end
And I have a category calling right method inside AFNetworking. To simplify it look like this:
-(void)urlString:(NSString*)urlString withMethod:(RequestMethodType)method parameters:(NSDictionary*)parameters success:(void (^)(NSURLSessionDataTask *operation, id responseObject))success failure:(void (^)(NSURLSessionDataTask *operation, NSError *error))failure {
switch (method) {
case GET: {
[self getWithURLString:urlString parameters:parameters success:^(NSURLSessionDataTask *operation, id responseObject) {
success(operation,responseObject);
} failure:^(NSURLSessionDataTask *operation, NSError *error) {
failure(operation,error);
}];
break;
}
}
And when I want to make request for example in my ViewController I make it like this:
[ResponseOrginizer getSth:^(NSURLSessionDataTask *operation, NSArray *locales, id plainObject) {
} failure:^(NSURLSessionDataTask *operation, NSError *error) {
}];
And when I run it in instrument I'm always getting:
And it here doesn't matter it will land on success/failure block, it always cause a leak. I extract everything from this and put it on github as simple as possible. Github link:
https://github.com/JakubMazur/SO41343532/
The leak appears here:
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
It seems that the reason is the same (or similar) as discussed here - NSURLSession holds a retained reference to the delegate.
Change you code in Connection.m like this to avoid the leak:
-(void)fireWithSuccess:(void (^)(NSURLSessionDataTask *operation, NSArray* returnArray, id originalResponse))success failure:(void (^)(NSURLSessionDataTask *operation, NSError *error))failure {
AFHTTPSessionManager *manager = [Connection manager];
[manager urlString:self.urlString withMethod:self.requestMethodType parameters:self.paramaters success:^(NSURLSessionDataTask *operation, id responseObject) {
success(operation,#[responseObject],nil);
} failure:^(NSURLSessionDataTask *operation, NSError *error) {
failure(operation,error);
}];
}
+ (AFHTTPSessionManager*) manager
{
static dispatch_once_t onceToken;
static AFHTTPSessionManager *manager = nil;
dispatch_once(&onceToken, ^{
manager = [AFHTTPSessionManager manager];
});
return manager;
}
If you need to handle multiple sessions, you may use another approach: call -[AFHTTPSessionManager invalidateSessionCancelingTasks:] when you're done with the session, e.g.:
-(void)fireWithSuccess:(void (^)(NSURLSessionDataTask *operation, NSArray* returnArray, id originalResponse))success failure:(void (^)(NSURLSessionDataTask *operation, NSError *error))failure {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager urlString:self.urlString withMethod:self.requestMethodType parameters:self.paramaters success:^(NSURLSessionDataTask *operation, id responseObject) {
success(operation,#[responseObject],nil);
[manager invalidateSessionCancelingTasks:YES];
} failure:^(NSURLSessionDataTask *operation, NSError *error) {
failure(operation,error);
[manager invalidateSessionCancelingTasks:YES];
}];
}
(Note: pass YES if you want to cancel pending tasks, NO otherwise).

AFNetworking 3.0 AFHTTPSessionManager using NSOperation

I'm stuck now some time and I need help. So in AFNetworking 2.0 we have AFHTTPRequestOperation so I could easily use NSOperationQueue and have some dependencies. So what we have now is only AFHTTPSessionManagerand NSURLSession that does not subclass NSOperation. I have class APIClient that subclasses AFHTTPSessionManager. I am using that class as singleton as sharedClient. I have overriden GET and POST so for example GET looks this:
- (NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(NSDictionary *)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure {
NSURLSessionDataTask *task = [super GET:URLString parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
success(task, responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
failure(task, [Response createErrorWithAFNetworkingError:error]);
}];
return task;
}
Do you have any idea how to implement in that manner (if it's possible) to wrap that as NSOperation? So what I want to do - I want to be able to run in parallel two network calls, and after that have another method call that depends on second network call of first two calls. Do you have any idea what would be best approach?
I've written a quick little set of classes (https://github.com/robertmryan/AFHTTPSessionOperation/) that wrap AFHTTPSessionManager requests in asynchronous NSOperation subclass. You can then use that to enjoy maxConcurrentOperation constraints, or operation dependencies.
For example, here's an example where we issue two concurrent requests and have a completion operation dependent upon completion of both of those requests:
// ViewController.m
#import "ViewController.h"
#import "AFNetworking.h"
#import "AFHTTPSessionOperation.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *urlString1 = #"...";
NSString *urlString2 = #"...";
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.name = #"AFHTTPSessionManager queue";
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"All done");
}];
NSOperation *op1 = [AFHTTPSessionOperation operationWithManager:manager HTTPMethod:#"GET" URLString:urlString1 parameters:nil uploadProgress:nil downloadProgress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"finished 1");
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"failed 1 - error = %#", error.localizedDescription);
}];
[completionOperation addDependency:op1];
NSOperation *op2 = [AFHTTPSessionOperation operationWithManager:manager HTTPMethod:#"GET" URLString:urlString2 parameters:nil uploadProgress:nil downloadProgress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"finished 2");
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"failed 2 - error = %#", error.localizedDescription);
}];
[completionOperation addDependency:op2];
[queue addOperations:#[op1, op2] waitUntilFinished:false];
[[NSOperationQueue mainQueue] addOperation:completionOperation]; // do this on whatever queue you want, but often you're updating UI or model objects, in which case you'd use the main queue
}
#end
It's worth noting that since you're only dealing with two requests, you could also use dispatch groups to accomplish the same thing:
// ViewController.m
#import "ViewController.h"
#import "AFNetworking.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *urlString1 = #"...";
NSString *urlString2 = #"...";
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[manager GET:urlString1 parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(#"finished 1");
dispatch_group_leave(group);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(#"failed 1 - error = %#", error.localizedDescription);
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[manager GET:urlString2 parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(#"finished 2");
dispatch_group_leave(group);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(#"failed 2 - error = %#", error.localizedDescription);
dispatch_group_leave(group);
}];
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(#"All done");
});
}
#end
With dispatch groups, you just need to be careful that every path within both the success and failure blocks call dispatch_group_leave.

AFNetworking 2.0 : AFHTTPRequestOperationManager asynchronous methode invoke

I am using AFNetworking 2.0 for networking of my application, I have implemented a method to check the login of a user
-(NSString *) loginWith : (NSString *) email andPassword :(NSString *) password
{
__block NSString * result ;
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSDictionary *params = #{#"email": email, #"password": password};
manager.responseSerializer = [AFJSONResponseSerializer serializer];
[manager POST:[kROOT_URL stringByAppendingString:#"auth/"] parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject)
{
NSLog(#"JSON: %#", (NSDictionary*)responseObject);
[[NSUserDefaults standardUserDefaults] setObject:responseObject forKey:USER_KEY];
[[NSUserDefaults standardUserDefaults] synchronize];
result = [responseObject objectForKey:#"id"];
NSLog(#"result :: %#",result);
} failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"Error: %#", error);
result = nil;
}];
return result;
}
and from other view controller I am invoking this method :
NSString * result = [self.user loginWith:emailCell.textField.text andPassword:passCell.textField.text];
NSLog(#"Result: %#",result);
the problem is that I get "null" as the object of result, This is because that when the NSlog is executed the network process was doing in background thread, so how I correct the implementation, so then I can get the result in the right moment, after fetching
Thank you
You should add a “callback“ block parameter to your method signature, and call that block when the request is done.
- (void)loginWith:(NSString *)email andPassword:(NSString *)password complete:(void(^)(id result, NSError *error))block {
// ...
[manager POST:[kROOT_URL stringByAppendingString:#"auth/"] parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject)
{
// ...
result = [responseObject objectForKey:#"id"];
if (block) block(result, nil)
} failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
if (block) block(nil, error)
}];
}
When calling the method, pass the callback block where you can read the result value:
[self.user loginWith:emailCell.textField.text andPassword:passCell.textField.text complete:^(id result, NSError *error) {
if (error)
NSLog(#"Error: %#", error);
else
NSLog(#"Result: %#", result);
}];

Always got nil value from the function in iOS

I'm using AFNetworking for some GET queries. But my function always returns nil value. Where I was wrong?
+ (NSString *)getRequestFromUrl:(NSString *)requestUrl {
NSString * completeRequestUrl = [NSString stringWithFormat:#"%#%#", BASE_URL, requestUrl];
__block NSString * results;
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:completeRequestUrl parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
results = [NSString stringWithFormat:#"%#", responseObject];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
results = [NSString stringWithFormat:#"Error"];
}];
NSLog(#"%#", results);
return results;
}
Thx! Artem.
You're not seeing a result because the blocks that you pass in for success and failure run asynchronously; by the time your NSLog gets called, the web service won't have returned yet. If you move your NSLog inside of your success and failure blocks, you should see a result get output to your console.
Because of the asynchronous nature of these calls, you won't be able to simply return the value from your method. Instead, you may want to take your own block as a parameter, which you can then call when you have a result. For example:
+ (void)getRequestFromUrl:(NSString *)requestUrl withCompletion:((void (^)(NSString *result))completion
{
NSString * completeRequestUrl = [NSString stringWithFormat:#"%#%#", BASE_URL, requestUrl];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:completeRequestUrl parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSString *results = [NSString stringWithFormat:#"%#", responseObject];
if (completion)
completion(results);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSString *results = [NSString stringWithFormat:#"Error"];
if (completion)
completion(results);
}];
}
You would then call your method like so:
[YourClass getRequestFromUrl:#"http://www.example.com" withCompletion:^(NSString *results){
NSLog(#"Results: %#", results);
}
AFNetworking's sample project has an example of using a block parameter to return a value from your web service calls: https://github.com/AFNetworking/AFNetworking/blob/master/Example/Classes/Models/Post.m
- (void)POST:(NSString *)URLString
parameters:(id)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
progress:(nullable void (^)(NSProgress * _Nonnull))uploadProgress
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
[_manager POST:[NSString stringWithFormat:#"%#%#",API_HOST,URLString] parameters:parameters constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
block(formData);
} progress:^(NSProgress * _Nonnull uploadProgress1) {
uploadProgress(uploadProgress1);
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
success(task,responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
failure(task,error);
}];
}

Resources