method returns before completionhandler - ios

I am developing a networkUtil for my project, I need a method that gets a url and returns the JSON received from that url using NSURLSessionDataTask to get a JSON from server. the method is the following:
+ (NSDictionary*) getJsonDataFromURL:(NSString *)urlString{
__block NSDictionary* jsonResponse;
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:urlString] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonResponse);
}];
[dataTask resume];
return jsonResponse;
}
The problem is that the completionHandler inside my method and the method itself are run on different threads and in the last line the jsonResponse is always nil
How should I set jsonResponse with returned json from urlString?
What is the best practice for this issue?

Block that is running in NSURLSession is running on different thread - your method doesn't wait block to finish.
You have two options here
First one. Send NSNotification
+ (void) getJsonDataFromURL:(NSString *)urlString{
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:urlString] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary* jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonResponse);
[[NSNotificationCenter defaultCenter] postNotificationName:#"JSONResponse" object:nil userInfo:#{#"response" : jsonResponse}];
}];
[dataTask resume];
}
Second one. Past completion block to this utility method
+ (void) getJsonDataFromURL:(NSString *)urlString
completionBlock:(void(^)(NSDictionary* response))completion {
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:urlString] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary* jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonResponse);
completion(jsonResponse);
}];
[dataTask resume];
}

Some people could say it is a horrible advice but you also can download your data synchronously. It should be done in a background queue. It is not a best practice but for some cases (like a command line utility, non-critical background queue) it is ok.
NSURLSession does not have synchronous download method but you can easily bypass it with semaphore:
+ (NSDictionary*) getJsonDataFromURL:(NSString *)urlString{
__block NSDictionary* jsonResponse;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); // Line 1
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:urlString] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonResponse);
dispatch_semaphore_signal(semaphore); // Line 2
}];
[dataTask resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // Line 3
return jsonResponse;
}
NSURLSession has a delegateQueue property which is used for "delegate method calls and completion handlers related to the session". By default NSURLSession always creates a new delegateQueue during initialisation. But if you set a NSURLSession's delegation queue yourself make sure you do not call your method in the same queue since it will block it.

It is obviously Method will return before Block is completed. Because that is main purpose of the block.
You needs to change something like this :
NSDictionary* jsonResponse;
+ (NSDictionary*) getJsonDataFromURL:(NSString *)urlString{
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:urlString] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
self.jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonResponse);
[dataTask resume];
// ad observer here that call method to update your UI
}];
}

This is the intended behavior of "asynchronous calls". They shall not block the calling thread, but execute the passed block, when the call is done.
Simply add the code that has to be executed after getting the result in the block.
So instead of …
+ (NSDictionary*) getJsonDataFromURL:(NSString *)urlString
{
__block NSDictionary* jsonResponse;
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:urlString] completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error)
{
jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonResponse);
}];
[dataTask resume];
return jsonResponse;
}
…
NSDictionary* jsonResponde = [self getJsonDataFromURL:url]; // BTW: get infringes the naming rules of Objective-C
// The code that has to be executed is here
… you do this:
+ (void) getJsonDataFromURL:(NSString *)urlString
{
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:urlString] completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error)
{
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonResponse);
// Place the code to be executed here <-----
}];
[dataTask resume];
}
…
[self getJsonDataFromURL:url]; // BTW: get infringes the naming rules of Objective-C
// No code here any more
If the code executed by -getJsonDataFromURL: depends on the caller, simply pass it as an argument to the method and execute it at the specified location. If you need help for that, let me know. I will add the code for it.
Another solution is to use a semaphore and wait, until the completion handler is executed. But this will block the UI and is the not-intended way to do it.

Related

NSUrlsession fired, url not called, nevertheless result

I have this method for getting JSON data from a URL:
-(void)getJsonResponse:(NSString *)urlStr success:(void (^)(NSDictionary *responseDict))success failure:(void(^)(NSError* error))failure
{
NSURLSession *session = [NSURLSession sharedSession];
NSURL *url = [NSURL URLWithString:urlStr];
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//NSLog(#"%#",data);
if (error) {
failure(error);
NSLog(#"Error: %#", [error localizedDescription]);
}
else {
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
//NSLog(#"%#",json);
success(json);
}
}];
[dataTask resume];
}
In myViewController, viewWillAppear I call this method as such:
NSString * URLString = #"my.valid.url";
[self getJsonResponse:URLString success:^(NSDictionary *result) {
//here some code when succesful
} failure:^(NSError *error) {
NSLog(#"Something terrible happened");
}];
}
That works fine, but only ONCE:
When I leave myViewController, and enter it again,
viewWillAppear is called, and subsequentially
[self getJsonResponse:... is called
my code in the success block is executed
However: monitoring the network activity with Charles, I notice, that no call is made to my.valid.url.
What gives? Should I invalidated the shared session? If so, when?
set NSURLSessionConfiguration chachePolicy to NSURLRequestReloadIgnoringCacheData and try again. Here is the good resource to understand about Http-caching. Read documentation given in apple guide as well.
NSURLSessionConfiguration *config = NSURLSessionConfiguration.defaultSessionConfiguration;
config.requestCachePolicy = NSURLRequestReloadIgnoringCacheData;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];

How to get NSDictionary from NSURLSessionDataTask

-(NSDictionary *)fetchFromUrl:(NSString *)url{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
dataFetched = [NSJSONSerialization JSONObjectWithData:data
options:0
error:NULL];
}];
[task resume];
NSLog(#"dataFetched, %#", dataFetched);
return dataFetched;
}
So I have tried putting the dataFetched as a global variable so I could access it around my .m file and make it accessible to other .m file but when I tried to NSLog the dataFetched from other .m file it outputs (null). Is there anyway I could make the data accessible throughout my other .m files that needed the data?
You need to use block with your method, instead of returning NSDictionary, So change your code like this.
First Change your method like this
-(void)fetchFromUrl:(NSString *)url withDictionary:(void (^)(NSDictionary* data))dictionary{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary *dicData = [NSJSONSerialization JSONObjectWithData:data
options:0
error:NULL];
dictionary(dicData);
}];
[task resume];
}
Now call your method like this
[self fetchFromUrl:urlStr withDictionary:^(NSDictionary *data) {
self.dataFetched = data;
NSLog(#"data %#",data);
}];
If you modify your NSDictionary inside the block you need declare __block attribute for your property like below
#property (nonatomic, strong) __block NSDictionary *dataFetched;
Look at doc
Use __block Variables to Share Storage If you need to be able to
change the value of a captured variable from within a block, you can
use the __block storage type modifier on the original variable
declaration. This means that the variable lives in storage that is
shared between the lexical scope of the original variable and any
blocks declared within that scope.

Does NSURLSession for HTTP data task (NSURLSessionDataTask) runs in background thread or we will have to provide the queue?

I started to use NSURLSession by avoiding NSURLConnection now a days as it's a new and elegant API provided by Apple. Previously, I used to put call NSURLRequest in GCD block to execute it in background. Here is how I used to do in past:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL *url = [NSURL URLWithString:#"www.stackoverflow.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLResponse *response;
NSError *error;
NSData *data = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
if (error) {
// Handle error
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
// Do something with the data
});
});
Now, here is how I use NSURLSession:
- (void)viewDidLoad
{
[super viewDidLoad];
/*-----------------*
NSURLSession
*-----------------*/
NSURL *url = [NSURL URLWithString:#"https://itunes.apple.com/search?term=apple&media=software"];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data
options:0
error:nil];
NSLog(#"%#", json);
}];
}
I want to know that, will my request be executed on background thread itself or I will have to provide my own mechanism same way I did in case of NSURLRequest ?
No, you don't need to use GCD to dispatch this to background queue. In fact, because the completion block runs on background thread, the exact opposite is true, that if you need anything in that block to run on the main queue (e.g., synchronized updates to model objects, UI updates, etc.), you have to manually dispatch that to the main queue yourself. For example, let's imagine that you were going to retrieve a list of results and update the UI to reflect this, you might see something like:
- (void)viewDidLoad
{
[super viewDidLoad];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:#"https://itunes.apple.com/search?term=apple&media=software"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// this runs on background thread
NSError *error;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
// detect and handle errors here
// otherwise proceed with updating model and UI
dispatch_async(dispatch_get_main_queue(), ^{
self.searchResults = json[#"results"]; // update model objects on main thread
[self.tableView reloadData]; // also update UI on main thread
});
NSLog(#"%#", json);
}];
[dataTask resume];
}

Downloading files in serial order using NSURLConnection in iOS

I want to download 3 files in serial order. Two of them are txt files and one is .gz file. I am using NSURLConnection to download the above files.
I am very new to iOS Programming. I have seen in other question in SO and google that we can use serial dispatch queue to do some operation serially.
But I don't know how to do this with NSURLConnection. I tried below but did not work.
dispatch_queue_t serialQueue = dispatch_queue_create("com.clc.PropQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
[self downloadProp];
});
dispatch_async(serialQueue, ^{
[self downloadDatabase];
});
dispatch_async(serialQueue, ^{
[self downloadTxt];
});
Above code is not executing connectionDidFinishLoading of NSURLCOnnection. Anyone has Idea how to achieve this?
NSURLSession provides a queue that will download each task in the order in which they are created.
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionTask *task1 = [session dataTaskWithURL:[NSURL URLWithString:#"http://yahoo.com"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(#"Complete 1");
}];
NSURLSessionTask *task2 = [session dataTaskWithURL:[NSURL URLWithString:#"http://msn.com"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(#"Complete 2");
}];
NSURLSessionTask *task3 = [session dataTaskWithURL:[NSURL URLWithString:#"http://google.com"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(#"Complete 3");
}];
// Regardless of which order the tasks are "resumed" (aka started) they will execute synchronously in the order added, above.
[task3 resume];
[task1 resume];
[task2 resume];
Update based on comments & chat:
To be more deterministic over the ordering & execution of tasks...
NSURLSession *session = [NSURLSession sharedSession];
__block NSURLSessionTask *task1 = nil;
__block NSURLSessionTask *task2 = nil;
__block NSURLSessionTask *task3 = nil;
task1 = [session dataTaskWithURL:urlToFirstFile completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// CHECK ERROR
NSLog(#"First file completed downloading");
[task2 resume];
}];
task2 = [session dataTaskWithURL:urlToSecondFile completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// CHECK ERROR
NSLog(#"Second file completed downloading");
[task3 resume];
}];
task3 = [session dataTaskWithURL:[NSURL URLWithString:#"http://google.com"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// CHECK ERROR
NSLog(#"Third file completed downloading");
}];
[task1 resume];
A simple recursive solution to ensure serial operation.
func serialisedRequests(session: URLSession, requests: [URLRequest], index: Int = 0) {
if index >= requests.count {
return
}
let task = session.dataTask(with: requests[index]) {
data, response, error in
serialisedRequests(session: session, requests: requests, index: index+1)
}
task.resume()
}
Simply set your NSURLSession's HTTPMaximumConnectionsPerHost property to 1 and add the tasks in the order you want.
See this answer for more details:
https://stackoverflow.com/a/21018964

How to return NSData from NSURLSessionDataTask completion handler

I am trying to make a simple class that I can use to call a post web service.
Everything is working perfectly except that I am not able to return the NSData.
This is my code:
+ (NSData *)postCall:(NSDictionary *)parameters fromURL:(NSString *)url{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
NSMutableArray *pairs = [[NSMutableArray alloc]init];
for(NSString *key in parameters){
[pairs addObject:[NSString stringWithFormat:#"%#=%#", key, parameters[key]]];
}
NSString *requestParameters = [pairs componentsJoinedByString:#"$"];
NSURL *nsurl = [NSURL URLWithString:url];
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:nsurl];
[urlRequest setHTTPMethod:#"POST"];
[urlRequest setHTTPBody:[requestParameters dataUsingEncoding:NSUTF8StringEncoding]];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//return data;
}];
[dataTask resume];
return nil;
}
Please notice that I have //return data but it gives me this error
Incompatible block pointer types sending 'NSData *(^)(NSData *__strong, NSURLResponse *__strong, NSError *__strong)' to parameter of type 'void (^)(NSData *__strong, NSURLResponse *__strong, NSError *__strong)'
My question is:
Is my way good or it will cause me problems in the future? I don't have image to download and I don't have anything to upload, I just have to send simple string data and receive simpe string data. Or it will be better to but that code in each function independently?
How can I return the data please?
You cannot just return the data (because the NSURLSessionDataTask runs asynchronously). You probably want to employ your own completion block pattern, similar to the completionHandler of the dataTaskWithRequest method.
So, you would add your own block parameter to your method, that you'll invoke from inside the dataTaskWithRequest method's completionHandler:
+ (NSURLSessionDataTask *)postCall:(NSDictionary *)parameters fromURL:(NSString *)url completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler {
// create your request here ...
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (completionHandler)
completionHandler(data, response, error);
}];
[dataTask resume];
return dataTask;
}
Or, because this dataTaskWithRequest runs on a background thread, it’s sometimes useful to make sure to dispatch the completion handler back to the main queue, e.g.
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (completionHandler)
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(data, response, error);
});
}];
Note, as an aside, I think it's good to return the NSURLSessionDataTask reference, like above, so (a) the caller can make sure the data task was successfully created; and (b) you have the NSURLSessionTask reference that you can use to cancel the task in case, at some future date, you want to be able to cancel the request for some reason (e.g. the user dismisses the view controller from which the request was issued).
Anyway, you'd then invoke this with:
NSURLSessionTask *task = [MyClass postCall:parameters fromURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// put whatever code you want to perform when the asynchronous data task completes
}];
if (!task) {
// handle failure to create task any way you want
}
You ask:
Is my way good or it will cause me problems in the future? I don't have [an] image to download and I don't have anything to upload, I just have to send [some] simple string data and receive [simple] string data. Or it will be better to but that code in each function independently?
If you're receiving simple string data back, I'd suggest composing your response in JSON format, and then having the completion block in postCall use NSJSONSerialization to extract the response. Using JSON makes it easier for the app to differentiate between successful response and a variety of server related problems that might also return string responses.
So, let's say you modified your server code to return a response like so:
{"response":"some text"}
Then you could modify postCall to parse that response like so:
+ (NSURLSessionDataTask *)postCall:(NSDictionary *)parameters fromURL:(NSString *)url completionHandler:(void (^)(NSString *responseString, NSError *error))completionHandler {
// create your request here ...
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (completionHandler) {
if (error) {
completionHandler(nil, error);
} else {
NSError *parseError = nil;
NSDictionary *responseDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
completionHandler(responseDictionary[#"response"], parseError);
}
}
}];
[dataTask resume];
return dataTask;
}
In terms of your underlying question, whether a method like postCall makes sense, yes, I think it makes perfect sense to put the details of creating the request in a single method. My minor reservation in your implementation was your decision to make it a class method rather than an instance method. You're currently creating a new NSURLSession for each request. I'd suggest making postCall an instance method (of a singleton if you want) and then saving the session as a class property, which you set once and then re-use on subsequent queries.
You should use a block method.
First define a block
typedef void (^OnComplete) (NSData *data);
Use the following method
+ (void)postCall:(NSDictionary *)parameters fromURL:(NSString *)url withBlock:(OnComplete)block; {
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
NSMutableArray *pairs = [[NSMutableArray alloc]init];
for(NSString *key in parameters){
[pairs addObject:[NSString stringWithFormat:#"%#=%#", key, parameters[key]]];
}
NSString *requestParameters = [pairs componentsJoinedByString:#"&"];
NSURL *myURL = [NSURL URLWithString:url];
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:myURL];
[urlRequest setHTTPMethod:#"POST"];
[urlRequest setHTTPBody:[requestParameters dataUsingEncoding:NSUTF8StringEncoding]];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
block(data);
}];
[dataTask resume];
}

Resources