NSURLSession synchronous calls - objective-c

I am not sure seen a clean answer to this. I know how to use blocks, and completion handlers, however for this I cannot use them and need to call the method synchronously :
- (void)codePathWithURL:(NSURLRequest *)request {
// Create session
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.URLCache = nil;
config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
NSURLSession *urlSession = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
// Using semaphores
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionDataTask *sessionTask = [urlSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
responseData = data;
httpResponse = (NSHTTPURLResponse *)response;
outError = error;
dispatch_semaphore_signal(semaphore);
}];
[sessionTask resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// do something with data
}
The problem I see here it is locks the main thread. How do I make this so that the main thread is not blocked?

Just a quick and dirty attempt to help you get going ... maybe use a condition?
// Call this on the background NOT on the main thread
- ( ... ) some message
{
// Added stuff
__block BOOL done = NO; // ... use now to set data as well
__block NSString * data; // ... or whatever is needed
NSCondition * condition = NSCondition.new;
// Your original stuff
// Create session
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.URLCache = nil;
config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
NSURLSession *urlSession = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *sessionTask = [urlSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
responseData = data;
httpResponse = (NSHTTPURLResponse *)response;
outError = error;
// Added stuff
condition.lock;
done = YES;
// Set data
data = ... something ...
condition.broadcast;
condition.unlock;
}];
[sessionTask resume];
// New stuff
condition.lock;
while ( ! done )
{
condition.wait;
}
condition.unlock;
// something like ...
return data;
...

Related

NSURLSessionDataTask completionHandler no longer executes when I switch to Release build configuration

I built a simple command line tool that fetches some data using an NSURLSessionDataTask. Now that I'm done coding, I find that the executable hangs when I switch to the Release build configuration in Xcode. I'm using a while loop that waits for the NSURLSessionData completionHandler to complete. The while loop works with Debug, but not with Release. If instead I use [NSThread sleepForTimeInterval:2.0f] to pause the code to wait for the completionHandler to complete, everything works fine in both Release and Debug, however I'd prefer to use the while loop as it is quicker and more logical.
Here is the relevant code:
// Configure Session
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
// Authentication for the session
NSString *auth = [[[NSString stringWithFormat:#"%#:%#",cpanelUser, WxWaPpIUPA] dataUsingEncoding:NSUTF8StringEncoding] base64EncodedStringWithOptions:0];
config.HTTPAdditionalHeaders = #{#"Authorization":[NSString stringWithFormat:#"Basic %#",auth]};
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
// Run Data Task
__block BOOL taskComplete = NO;
NSURLSessionDataTask *task = [session dataTaskWithURL:urlComponents.URL
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
if(error) {
// *HTTP response failed
NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
[result addEntriesFromDictionary:queryReturn(EMAIL_TYPE_ERROR, [NSString stringWithFormat:#"There was a connection issue with the session data task.<br><br>Error: %# HTTP Status Code for the <a href='%#'>URL</a> requested: %li.",error,urlComponents.URL,statusCode])];
} else {
NSError *jsonError;
NSDictionary *jsonObject = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingAllowFragments
error: &jsonError];
if(!jsonObject){
// *JSON parsing Error
[result addEntriesFromDictionary:queryReturn(EMAIL_TYPE_ERROR, [NSString stringWithFormat:#"There was an error while parsing the JSON data returned from cPanel.<br><br>Error: %#",jsonError])];
} else {
if([jsonObject[#"cpanelresult"] objectForKey:#"error"] != nil) {
// *cPanel API returned an Error
NSString *cpanelError = [NSString stringWithFormat:#"%#",[jsonObject[#"cpanelresult"] objectForKey:#"error"]];
if(verbose) NSLog(#"cPanel API 2 Error: %#\n", cpanelError);
[result addEntriesFromDictionary:queryReturn(EMAIL_TYPE_ERROR, [NSString stringWithFormat:#"cPanel API 2 returned an error while executing function '%#'.<br><br>Error: %#",funct, cpanelError])];
} else {
[result addEntriesFromDictionary:jsonObject];
}
}
}
taskComplete = YES;
}];
[task resume];
// Wait for task to complete
while(!taskComplete);
//[NSThread sleepForTimeInterval:2.0f];

synchronous NSURLSessionDataTask using objective-c

I'm try to do synchronous NSURLSessionDataTask with the below code but unable to proceed.
__block NSData *rData = nil;
__block BOOL taskDone = NO;
__block NSData *rError = nil;
NSURL *url = [NSURL URLWithString:dataURL];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:1 timeoutInterval:30];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration] delegate:nil delegateQueue:nil];
NSURLSessionDataTask *taskData = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
rData = [NSData dataWithData:data];
rError = [error copy];
taskDone = YES;
}];
[taskData resume];
while (taskDone == NO) {
if (_close == YES) {
[taskData cancel];
return nil;
}
usleep(20000);
}
I need to synchronous call so that I can remove the while loop which is not needed.
Below is my code with synchronous call using semaphore
dispatch_semaphore_t sem;
__block NSData *rData = nil;
__block BOOL taskDone = NO;
__block NSData *rError = nil;
NSURL *url = [NSURL URLWithString:dataURL];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:1 timeoutInterval:30];
// creating semaphore
    sem = dispatch_semaphore_create(0);
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration] delegate:nil delegateQueue:nil];
NSURLSessionDataTask *taskData = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
rData = [NSData dataWithData:data];
rError = [error copy];
taskDone = YES;
//call semaphore
        dispatch_semaphore_signal(sem);
}];
[taskData resume];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_release(sema);
// THIS part not sure... how can we accommodate this below code
while (taskDone == NO) {
if (_close == YES) {
[taskData cancel];
return nil;
}
usleep(20000);
}
above code could be correct ?
I understand that what you want to do is wait for the DataTask to be completed before continue with you code, the best way is to put your request in a function with a completionHandler.
First create a function that will return a NSURLSessionDataTask with a completion handler:
-(NSURLSessionDataTask*)startSessionDataTaskWithCompletionHandler:(void (^)(NSData *myData))completionBlock {
//Set your request
NSString *dataURL = #"www.yoururl.com";
NSURL *url = [NSURL URLWithString:dataURL];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:1 timeoutInterval:30];
// I recommend to use sharedSession because is a simple request, so its not needed a specific session configuration.
NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession] dataTaskWithRequest: request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
if (completionBlock){
completionBlock(data);
return;
//When you call this function, the completionBlock will use this data
}
} else {
//Error handle
return;
}
}];
[dataTask resume];
return dataTask;
}
Then you can call this function from anywhere:
NSURLSessionTask *task = [self startSessionDataTaskWithCompletionHandler:^(NSData *myData) {
// put whatever code you want to perform when the asynchronous data task finish, for example:
rData = [NSData dataWithData:myData];
}];
if (!task) {
// handle failure to create task any way you want
}
You can make NSURLSessionDataTask synchronous with PromiseKit. Install it manually or add the following line to the Podfile if you use CocoaPods (tested with CocoaPods 1.7.3):
pod "PromiseKit", "6.10.0"
Add the following line to the top of the code file:
#import PromiseKit;
Then create a wrapper for your task:
- (AnyPromise*)promiseToLoadData:(NSString*)dataURL {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver _Nonnull resolver) {
NSURL *url = [NSURL URLWithString:dataURL];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:1 timeoutInterval:30];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration] delegate:nil delegateQueue:nil];
NSURLSessionDataTask *taskData = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error != nil) {
resolver([error copy]);
} else {
resolver([NSData dataWithData:data]);
}
}];
[taskData resume];
}];
}
Use wait to resolve the promise synchronously:
id value = [self promiseToLoadData:#"http://your.url"].wait;
if ([value isKindOfClass:[NSData class]]) {
NSLog(#"%#", [[NSString alloc] initWithData:value encoding:NSUTF8StringEncoding]);
}

Asynchronously Api calls and returning data outside the block

I don't understand why I am getting null array outside the block code, even though I am using __block keyword on my array.
I am successfully getting data from a backend api with following code
`-(void)getJsonResponse:(NSString *)urlStr success:(void (^)(NSArray *responseDict))success failure:(void(^)(NSError* error))failure
{
NSURLSession *session = [NSURLSession sharedSession];
NSURL *url = [NSURL URLWithString:urlStr];
// Asynchronously API is hit here
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// NSLog(#"%#",data);
if (error)
failure(error);
else {
NSArray *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
// NSLog(#"%#",json);
success(json);
}
}];
[dataTask resume]; // Executed First
}`
Then in my function for returning the data I am using following
`- (NSArray *)get_data:(NSDictionary *)credentials{
NSString *urlStr =[ NSString stringWithFormat:#"http://test.com %#",credentials];
__block NSArray *jsonArray= [[NSArray alloc]init];
[self getJsonResponse:urlStr success:^(NSArray *responseArray) {
jsonArray = responseArray;
NSLog(#"%#",responseArray);
} failure:^(NSError *error) {
// error handling here ...
}];
NSLog(#"%#",jsonArray);
return jsonArray;
}
`
The issue here is although I am successfully getting data within getJsonResponse block, but when I am trying to return the response data array as function return I am getting null for jsonArray. I thought assigning __block infront of jsonArray should retain the data assign within the block code ?
The second approach is not to use Async way like following
`- (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];
}
*response = resp;
*error = err;
return data;
}`
That way its blocking the main thread whilst waiting for data.
I would suggest using the same approach of getJsonResponse for your get_data function:
- (void)get_data:(NSDictionary *)credentials finish:(void(^)(NSArray *data))finish{
NSString *urlStr =[ NSString stringWithFormat:#"http://test.com %#",credentials];
__block NSArray *jsonArray= [[NSArray alloc]init];
[self getJsonResponse:urlStr success:^(NSArray *responseArray) {
jsonArray = responseArray;
if (finish) {
finish(jsonArray);
}
} failure:^(NSError *error) {
// error handling here ...
}];
}

Call a block body of the method dataTaskWithRequest consistently

for example:
I have a function
-(void) someFunc:(NSString *) searchRequest {
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[[session dataTaskWithRequest:request
completionHandler:^(NSData *data,NSURLResponse *response, NSError *error)
{
// body block
}
]
resume];
}
and I call someFunc in:
- (IBAction)searchButtonPressed:(id)sender {
NSString * searchRequest = #"blablabla";
[self someFunc:searchRequest];
}
In this case, I realized that the body of the block is executed in a separate thread, which leads to data loss.
I want all actions are performed sequentially. How to do it?
If you want someFunc: to return a value look into using a dispatch_semaphore_t to block the calling thread until the request is finished and assign the return value to a __block qualified variable.
You could also modify someFunc: to take a block as an additional argument that would be invoked when the request completes.
- (void)someFunc:(NSString *)searchRequest completion:(void(^)(id result))completion {
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[[session dataTaskWithRequest:request
completionHandler:^(NSData *data,NSURLResponse *response, NSError *error)
{
completion(data);
}]
resume];
}

NSURLSessionDataTask not executing the completion handler block

I am trying to pull some data from my local node server. The server is getting the get request and logging it, but for some reason my iOS app will not execute any of the code that I have in the completion handler. Here is the code:
- (IBAction) buttonPressed{
NSURL *url = [NSURL URLWithString:#"http://127.0.0.1:3000/"];
NSURLSessionDataTask *dataTask =
[self.session dataTaskWithURL:url
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error){
nameLabel.text = #"yay!";
/*
if (!error){
nameLabel.text = #"noerr";
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse *)response;
if (httpResp.statusCode == 200){
NSError *jsonErr;
NSDictionary *usersJSON =
[NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingAllowFragments
error:&jsonErr];
if (!jsonErr){
// nameLabel.text = usersJSON[#"username"];
nameLabel.text = #"nojerr";
}
else{
nameLabel.text = #"jsonErr";
}
}
}
else{
nameLabel.text = #"Err";
}
*/
}];
[dataTask resume];
}
When the program is run, the nameLabel is not changed to "yay". However if I try to change the nameLabel before the NSURLSessionDataTask line, it changes.
NSURLSessionDataTask runs in a background thread. To update anything in the user interface such as labels, buttons, table views, etc, you must do so on the main thread. If you want to update the label text from the completionHandler block then you need to update the label in the main thread like so:
dispatch_sync(dispatch_get_main_queue(), ^{
nameLabel.text = #"yay!";
});
try this magic:
static NSURLSession* sharedSessionMainQueue = nil;
if(!sharedSessionMainQueue){
sharedSessionMainQueue = [NSURLSession sessionWithConfiguration:nil delegate:nil delegateQueue:[NSOperationQueue mainQueue]];
}
NSURLSessionDataTask *dataTask =
[sharedSessionMainQueue dataTaskWithURL:url completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error){
//now will be on main thread
}];
[dataTask resume];
This gives you the original behavior of NSURLConnection with the completing handler on the main thread so you are safe to update the UI. However, say you would like to parse the download or do some heavy processing, in that case you might benefit from the completion handler on the operation queue's background thread and then using dispatch_sync to the main thread as a final step.