How can I wait for finish of dataTaskWithRequest ? I need to perform some tasks after network fetch is completely over.
If you really need synchronous request, you can use semaphores.
I've implemented a small category on NSURLSession to provide this functionality.
In .h file:
#import Foundation.NSURLSession;
#interface NSURLSession (Additions)
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error;
#end
In .m file:
#implementation NSURLSession (Additions)
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse *__autoreleasing *)responsePointer error:(NSError *__autoreleasing *)errorPointer
{
dispatch_semaphore_t semaphore;
__block NSData *result = nil;
semaphore = dispatch_semaphore_create(0);
void (^completionHandler)(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error);
completionHandler = ^(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error)
{
if ( errorPointer != NULL )
{
*errorPointer = error;
}
if ( responsePointer != NULL )
{
*responsePointer = response;
}
if ( error == nil )
{
result = data;
}
dispatch_semaphore_signal(semaphore);
};
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:completionHandler] resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return result;
}
#end
I think, that method is obvious inside the class
NSURLSessionDataTask *task = [defaultSession dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
{
// code after completion of task
}];
[task resume];
- (void) loginRequest:(NSString*) username withPassword:(NSString *) password callback:(void (^)(NSError *error, BOOL success))callback
{
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
if (error) {
// Handle error, optionally using
callback(error, NO);
}
else {
callback(nil, YES);
}
}];
[dataTask resume];
}
Call this method like so:
[self loginRequest:#"myUsername" password:#"password" callback:^(NSError *error, BOOL success) {
if (success) {
NSLog(#"My response back from the server after an unknown amount of time");
}
else {
NSLog(#"%#", error);
}
}];
lets just do your tasks in the completion block of dataTaskWithRequest.
Until than you can display an activity indicator to block the user from touching anything on the screen.
Example:
activityIndicator.startAnimating()
let task : NSURLSessionDataTask = session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
activityIndicator.stopAnimating()
// do your stuff here
});
Related
I am new to blocks. I am trying to analyze how the following code works.
As i understand this specific method has a block and returns a
NSURLSessionDataTask
. getTotalFollowersFrom is the name of the method. (NSString*)influencerId is the userId that is passed when calling this method. WithCompletion: is used so we know when the method completes the Api call. Void is what the block returns. The caret symbol (^) is used to define the block. The following (id _Nullable likesCountRequestResponse, NSError * _Nullable error)completion are arguments. This is what i do not understand. According to documentation arguments have return values inside the block. I see the return result; that returns the NSURLSessionDataTask but i do not understand how the values return for the arguments of the block. What am i missing? Could anyone please explain to me?
- (NSURLSessionDataTask *)getTotalLikesFrom:(NSString*)userId withCompletion:(void (^)(id _Nullable likesCountRequestResponse, NSError * _Nullable error))completion {
NSString* postString = [NSString stringWithFormat:#"apicall/%#", userId];
#weakify(self)
NSURLSessionDataTask *result = [self GET:postString parameters:nil completion:^(OVCResponse * _Nullable response, NSError * _Nullable error) {
#strongify(self)
[self handleResponse:response error:error adjustErrorBlock:self.commonValidationErrorAdjustBlock completion:completion];
}];
return result;
}
- (void)handleResponse:(nullable OVCResponse *)response error:(nullable NSError *)error adjustErrorBlock:(nullable ApiClientv2AdjustErrorBlock)adjustErrorBlock completion:(void (^)(id _Nullable result, NSError * _Nullable error))completion
{
if (response.HTTPResponse.statusCode >= 500)
{
error = nil != error ? error : NSError.com_eight_APIServiceUnknownError; [SentrySDK captureError:error];
}
else
{
error = nil != error ? error : NSError.com_eight_APIServiceUnknownError;
}
id result = nil == error ? response.result : nil;
completion(result, error);
}
Your example is not complete. So, let's consider a MCVE:
- (NSURLSessionDataTask *)networkRequestWithCompletion:(void (^)(NSDictionary * _Nullable object, NSError * _Nullable error))completion {
NSURL *url = [NSURL URLWithString:#"https://httpbin.org/get"];
NSURLSessionDataTask *task = [NSURLSession.sharedSession dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!data || error) {
completion(nil, error);
return;
}
NSError *parseError;
NSDictionary *resultObject = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&parseError];
if (parseError) {
completion(nil, parseError);
return;
}
completion(resultObject, nil);
}];
[task resume];
return task;
}
This performs a network request and parses the JSON. Inside that method, you will see references to:
completion(nil, error);
Or:
completion(resultObject, nil);
That is how the data is passed back to the caller. The method supplies the parameters of the block when it calls completion . Thus, this method can be supplied a block and use these two parameters:
[self networkRequestWithCompletion:^(NSDictionary *dictionary, NSError *error) {
if (error) {
NSLog(#"error = %#", error);
return;
}
// you can access the `dictionary` parameter here ...
NSLog(#"dictionary = %#", dictionary);
}];
// ... but you cannot reference the `dictionary` or `error` parameters here
// because the above runs asynchronously (i.e., later).
In the code snippet you supplied in your question, you are not calling completion anywhere. But notice that it is supplied to handleResponse. That method is undoubtedly calling the block for you.
As a matter of personal preference, I think that the choice of result for the NSURLSessionDataTask is a little confusing. So in my example, I gave it a better name, task, to make it clear that it is the NSURLSessionTask object, not the result of the network request.
But what is the purpose of this object? It is so that the caller has the option to cancel the request if it wants. For example, you might have:
#interface ViewController ()
#property (nonatomic, weak) NSURLSessionTask *task;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.task = [self networkRequestWithCompletion:^(NSDictionary *dictionary, NSError *error) {
if (error) {
NSLog(#"error = %#", error);
return;
}
NSLog(#"dictionary = %#", dictionary);
}];
}
- (IBAction)didTapCancelButton:(id)sender {
[self.task cancel];
}
...
#end
So, the NSURLSessionTask reference is returned, which we save, so that we can have, for example, a cancel button that will cancel that asynchronous network request.
But, in short, do not conflate the NSURLSessionTask reference that is immediately returned by networkRequestWithCompletion (to give us the option to cancel the request later) with the parameters of the block, which we use to supply the caller with the results of the network request.
Right now I am developing I little class that has a method for sending a POST request. This method is intended for returning a ResponseModel (which basically has two ivars: code, message), this model is going to be map from response.
I am using dataTaskWithRequest:urlRequest completionHandler: method. Like this:
+ (void)sendPOSTRequest1:(id)data withResponse:(void (^) (ResponseModel * data) )taskResponse {
NSError *error = nil;
NSMutableURLRequest * urlRequest = [self getRequestObject];
[urlRequest setHTTPMethod:#"POST"];
NSData * requestData = [self encodeAndEncrypt:data];
[urlRequest setHTTPBody:requestData];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session
dataTaskWithRequest:urlRequest
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
ResponseModel * responseModel = [NSKeyedUnarchiver
unarchivedObjectOfClass:[ResponseModel class]
fromData:data
error:&error];
taskResponse(responseModel);
}];
[dataTask resume];
}
And call the method this way:
DummyModel * dummy = [[DummyModel alloc] init];
__block ResponseModel * result = [[ResponseModel alloc] init];
[HTTPRequest sendPOSTRequest1:dummy withResponse:^(ResponseModel *data) {
result = data;
NSLog(#"data %#",data);
}];
// It`s not sure that the asyncronous request has already finished by this point
NSLog(#"POST result : %#",result);
My problem is that I do not want to execute a code in call back block because I need to wait for the response in order to return a ResponseModel and whoever is implementing this can receive the Model and make other stuff.
I been researching for using NSURLConnection because it has a method for executing Synchronous request, but now It´s deprecated, so I been wondering: is It a way I can wait for a response using what I have in the code ?
You can use GCD to implement synchronous request like this:
swift code
public static func requestSynchronousData(request: URLRequest) -> Data? {
var data: Data? = nil
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
let task = URLSession.shared.dataTask(with: request, completionHandler: {
taskData, _, error -> () in
data = taskData
if data == nil, let error = error {print(error)}
semaphore.signal()
})
task.resume()
_ = semaphore.wait(timeout: .distantFuture)
return data
}
Objective-C code
+ (NSData *)requestSynchronousData:(NSURLRequest *)request {
__block NSData * data = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable taskData, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(#"%#", error);
}
data = taskData;
dispatch_semaphore_signal(semaphore);
}];
[task resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return data;
}
You can use dispatch_async to handle UI interaction inside the block
DummyModel * dummy = [[DummyModel alloc] init];
__block ResponseModel * result = [[ResponseModel alloc] init];
[HTTPRequest sendPOSTRequest1:dummy withResponse:^(ResponseModel *data) {
result = data;
dispatch_async(dispatch_get_main_queue(), ^{
// handle some ui interaction
});
NSLog(#"data %#",data);
}];
I have a method, the method have return the nsdata value, but I don't known how to get the return value from NSURLSessionDataTask block. and how to call the getDownloadFileData methods.Code for task is :-
caller:
NSData *getFileDataResult = [self getDownloadFileData:pathString];
method:
- (NSData*) getDownloadFileData : (NSString*) filePath {
NSURLSessionDataTask *downloadFile = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:filePath] completionHandler:^(NSData *fileData, NSURLResponse *response, NSError *error){
// .....
// fileData should return out.
[downloadFile resume];
});
// I want to return the fileData after download completion.
// how to return?
}
Have anyone can give me a hand?
Thank you very much.
Please check my answer, I hope this helpful
- (NSData *)getDownloadFileData:(NSString *)filePath {
__block NSData *responseData = nil;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSURLSessionDataTask *downloadFile = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:filePath] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
responseData = data;
dispatch_semaphore_signal(sema);
}];
[downloadFile resume];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
return responseData;
}
- (void)whereToCall {
// Because to prevent the semaphore blocks main thread
dispatch_queue_t myQueue = dispatch_queue_create("com.abc", 0);
dispatch_async(myQueue, ^{
NSData *data = [self getDownloadFileData:#"urlString"];
});
}
- (void)betterGetDownloadFileData:(NSString *)filePath completion:(void (^)(NSData * __nullable data))completion {
NSURLSessionDataTask *downloadFile = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:filePath] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (completion) {
completion(data);
}
}];
[downloadFile resume];
}
I recommend you should design your code as my suggestion that using block instead.
First of all you have put resume method at wrong place. It should be like this:
- (NSData*) getDownloadFileData : (NSString*) filePath {
NSURLSessionDataTask *downloadFile = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:filePath] completionHandler:^(NSData *fileData, NSURLResponse *response, NSError *error){
// .....
// fileData should return out.
});
[downloadFile resume];//NOTICE THE PLACEMENT HERE
// I want to return the fileData after download completion.
// how to return?
}
Second thing is , you can simply create a NSData variable and assign it the value in completion block rather than passing data back.
OR
Simply do like this in completion block
if(fileData){
return fileData;
}
I had iOS framework which it send JSON to server using NSURLSessionDataTask like this :
NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
int responseStatusCode = [httpResponse statusCode];
if (responseStatusCode == 200)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate onJsonHttpResult:data andStatusResponse:responseStatusCode];
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate onJsonHttpResult:nil andStatusResponse:responseStatusCode];
});
}
}];
[postDataTask resume];
but whenever I run it, [self.delegate onJsonHttpResult:nil andStatusResponse:responseStatusCode]; not called.
is there any way to get value outside NSURLSessionDataTask when it run inside the framework ?
Thanks
My suggestion is to create and use APIHelperClass with completionBlock.
That will be more easy and affective then use of custom Delegate as per my view.
To create it you can do as follow:
In APIHelperClass.h
#import <Foundation/Foundation.h>
#interface APIHelperClass : NSObject
+(void)apiCallSharedSessionPOST:(NSURLRequest *)request withCompletionHandlar:(void (^) (NSDictionary *dicResult,NSError *error, int status))completionBlock;
#end
And
APIHelperClass.m
#import "APIHelperClass.h"
#implementation APIHelperClass
+(void)apiCallSharedSessionPOST:(NSURLRequest *)request withCompletionHandlar:(void (^) (NSDictionary *dicResult,NSError *error, int status))completionBlock;
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: nil delegateQueue: [NSOperationQueue mainQueue]];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
int responseStatusCode = (int)[httpResponse statusCode];
if (error!=nil)
{
completionBlock(nil,error,responseStatusCode);
[task suspend];
}
else
{
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
completionBlock(dic,error,responseStatusCode);
[task suspend];
}
}];
[task resume];
}
Then you can use that class for whole App and you don't need to create custom Delegates always.
Then Use that where you want Like :
NSURLRequest *request;
/*
Configure your Request Here
*/
[APIHelperClass apiCallSharedSessionPOST:request withCompletionHandlar:^(NSDictionary *dicResult, NSError *error, int status) {
}];
Thanks for help but I still need delegate to return my value outside framework.
I got issued that whenever I done with NSURLSessionDataTask, delegate is became null, I think it cause that delegate already released after I got response from NSURLSessionDataTask, So I tried to change #property delegate to strong and it work. I can return my value using delegate again. Thanks
Warning:'sendSynchronousRequest(_:returningResponse:)' was deprecated
in iOS 9.0: Use [NSURLSession dataTaskWithRequest:completionHandler:]
(see NSURLSession)
urlData = try NSURLConnection.sendSynchronousRequest(request, returningResponse:&response)
Any idea on how to get rid of this warning? I just upgraded from Swift 1.2 to Swift 2
UPDATE: Fonix is marked as my best answer. If your trying to add a try statement, I modified his answer as followed:
urlData = try NSURLSession.dataTaskWithRequest(<#request: NSURLRequest#>, completionHandler: <#((NSData!, NSURLResponse!, NSError!) -> Void)?##(NSData!, NSURLResponse!, NSError!) -> Void#>)
Use NSURLSession instead like below,
For Objective-C
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:londonWeatherUrl]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
// handle response
}] resume];
For Swift,
var request = NSMutableURLRequest(URL: NSURL(string: "YOUR URL"))
var session = NSURLSession.sharedSession()
request.HTTPMethod = "POST"
var params = ["username":"username", "password":"password"] as Dictionary<String, String>
var err: NSError?
request.HTTPBody = NSJSONSerialization.dataWithJSONObject(params, options: nil, error: &err)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
println("Response: \(response)")})
task.resume()
I wrote the following solution for the cases when you actually need for synchronous request which blocks the current thread execution. I use this code for migration from NSURLConnection to NSURLSession in the complex solution where it was quite a hassle to just change to async approach. With this solution the migration is just method name replacement.
NOTE: If you have a simple case, please use the accepted answer instead.
- (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error
{
NSError __block *err = NULL;
NSData __block *data;
BOOL __block reqProcessed = false;
NSURLResponse __block *resp;
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable _data, NSURLResponse * _Nullable _response, NSError * _Nullable _error) {
resp = _response;
err = _error;
data = _data;
reqProcessed = true;
}] resume];
while (!reqProcessed) {
[NSThread sleepForTimeInterval:0.02];
}
if (response != nil)
*response = resp;
if (error != nil)
*error = err;
return data;
}
Usage (simple replace NSURLConnection to this method):
//NSData *data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:&resp error:&err];
NSData *data = [self sendSynchronousRequest:theRequest returningResponse:&resp error:&err];
If you need to block the current thread (like Mike Keskinov's answer), best to use gdc semaphore instead of doing a [NSThread sleepForTimeInterval:0]. e.g.
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:londonWeatherUrl]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
// handle response
dispatch_semaphore_signal(semaphore);
}] resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
and Swift (tested on 5.0):
let semaphore = DispatchSemaphore(value:0)
URLSession.shared.dataTask(with: serverUrl) { (httpData, response, error) in
// handle response
semaphore.signal()
}.resume()
semaphore.wait()
I have modified the code of Nilesh Patel a little bit, so you can use the old call, just by changing class name.
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error {
__block NSData *blockData = nil;
#try {
__block NSURLResponse *blockResponse = nil;
__block NSError *blockError = nil;
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable subData, NSURLResponse * _Nullable subResponse, NSError * _Nullable subError) {
blockData = subData;
blockError = subError;
blockResponse = subResponse;
dispatch_group_leave(group);
}] resume];
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
*error = blockError;
*response = blockResponse;
} #catch (NSException *exception) {
NSLog(#"%#", exception.description);
} #finally {
return blockData;
}
}
Swift 4 / Xcode 9
If you really want the request to be synchronous like in the deprecated semantics, you can block the main thread with an empty loop on a condition set true by the completion handler:
let request = URLRequest(url: URL(string: "YOUR_URL")!)
let session = URLSession.shared
var gotResp = false
let task = session.dataTask(with: request,
completionHandler: { data, response, error -> Void in
// do my thing...
gotResp = true
})
task.resume()
// block thread until completion handler is called
while !gotResp {
// wait
}
print("Got response in main thread")
...
EDIT: or if you prefer to use semaphores like in the Obj-C Nick H247 answer:
let request = URLRequest(url: URL(string: "YOUR_URL")!)
let session = URLSession.shared
let ds = DispatchSemaphore( value: 0 )
let task = session.dataTask(with: request,
completionHandler: { data, response, error -> Void in
// do my thing..., then unblock main thread
ds.signal()
})
task.resume()
// block thread until semaphore is signaled
ds.wait()
print("Got response in main thread")
...
Here is a complete version of mine with dispatch_semaphore_t and return response and error without block assign warning . Thank #Nick H247 and #Mike Keskinov.
- (NSData*)sendSynchronousRequest:NSURLRequest *urlRequest
returningResponse:(NSURLResponse **)outResponse
error:(NSError **)outError
{
NSError __block *err = NULL;
NSData __block *data;
BOOL __block reqProcessed = false;
NSURLResponse __block *resp;
// data = [NSURLConnection sendSynchronousRequest:urlRequest returningResponse:response error:error];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSession *session = _session;
[[session dataTaskWithRequest:urlRequest
completionHandler:^(NSData *_data,
NSURLResponse *_response,
NSError *_error) {
// handle response
data = _data;
resp = _response;
err = _error;
reqProcessed = true;
dispatch_semaphore_signal(semaphore);
}] resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
if (reqProcessed) {
if(outResponse != NULL) {
*outResponse = resp;
}
if (outError != NULL) {
*outError = err;
}
}
return data;
}
you can hide that warning in your project by using following code write your method between input directives and your warning goes away.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
- (void)yourMethodToCallNSURLConnection {
//use deprecated stuff
}
#pragma GCC diagnostic pop