I am working on a project where I call one method from another. In the 2nd method I fetch data from a server using a NSURLSession. When the 2nd method returns the NSData to the first method, the data is converted into JSON and then returned to the viewcontroller that made the inital call on the first method. The problem I am having is that the first method is returning a null object because the NSData doesn't load fast enough. I'm not sure what to do about it.
Here is the code:
Method 1
-(NSDictionary*)returnJsonDictionaryFromUrl:(NSURL*)url {
NSData *data = [self makeHttpRequestWithUrl:url];
//NSLog(#"Data is: %#", data);
NSError *error;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data
options:kNilOptions error:&error];
return json;
}
Method 2
-(NSData*)makeHttpRequestWithUrl:(NSURL*)url {
if (!_data) {
_data = [[NSData alloc]init];
}
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:url
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
_data = data;
}] resume];
return _data;
}
Thanks in advance for any suggestions!
So blocks by default are skipped over in execution and queued up (sometimes on other threads). This means when you're returning a variable that you had just set in a block, you should assume the block has not been executed and any variables you set inside it will not be set until much later.
The best way to handle this is to pass in the completion block to the method. So instead of having it return the NSData pointer, instead make it a void and simply pass in the completion block to that. This will be your request method:
-(void)makeHttpRequestWithUrl:(NSURL*)url completion:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completion {
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:url
completionHandler:completion] resume];
}
And you'll call it like this:
[object makeHttpRequestWithUrl:url completion:^(NSData *data, NSURLResponse *response, NSError *error) {
// handle error
// use data
}
If you really must do a synchronous web request NSURLSession is not the right tool. For this we have NSURLConnection sendSynchronousRequest:returningResponse:error:. And if you don't even care about the HTTP response you also can use NSData dataWithContentOfURL:.
But you really shouldn't use those, especially on the main thread as this will cause your app to crash if the network request takes too long. Embrace the asynchronous nature of network requests and handle your data in the completion block as kpsharp suggests in his answer.
Related
I handle some old code, it runs well, but now crash only on ios 14
here is the demo
static NSData *DownloadWithRange(NSURL *URL, NSError *__autoreleasing *error) {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:URL];
request.timeoutInterval = 10.0;
__block NSData *data = nil;
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
NSURLSession *URLSession = [NSURLSession sessionWithConfiguration:config];
NSURLSessionDataTask *task = [URLSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable taskData, NSURLResponse * _Nullable response, NSError * _Nullable taskError) {
data = taskData;
if (error)
*error = taskError;
dispatch_semaphore_signal(sema);
}];
[task resume];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
return data;
}
- (IBAction)crashButton:(id)sender {
NSURL *url = [NSURL URLWithString:#"http://error"];
NSError * error = nil;
NSData *compressedData = DownloadWithRange(url, &error);
NSLog(#"error is %#",error);
}
before DownloadWithRange returned, the taskError memory(NSURLError) has released
on ios 13, it don't crash
it's really weird
The zombie diagnostics are letting you know that the autorelease object is getting deallocated by the time the data is returned. You should not be instantiating an autorelease object in one thread and trying to have a pool on a separate thread manage that. As the docs say:
Autorelease pools are tied to the current thread and scope by their nature.
While the problem might be manifesting itself differently in iOS 14, I do not believe that this pattern was ever acceptable/prudent.
If you're going to use this pattern (which I wouldn't advise; see below), you can solve this problem by copying the error object on the calling thread before returning:
static NSData *DownloadWithRange(NSURL *URL, NSError * __autoreleasing *error) {
...
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
if (error) {
*error = [*error copy];
}
return data;
}
FWIW, this technique of using semaphore to make asynchronous method behave synchronously is generally considered an anti-pattern. And you definitely should never use this pattern from the main thread.
I would suggest adopting asynchronous patterns:
- (NSURLSessionTask *)dataTaskWithURL:(NSURL *)url completion:(void (^ _Nonnull)(NSData * _Nullable data, NSError * _Nullable error))completion {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
request.timeoutInterval = 10.0;
NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(data, error);
});
}];
[task resume];
[session finishTasksAndInvalidate];
return task;
}
And
[self dataTaskWithURL:url completion:^(NSData * _Nullable data, NSError * _Nullable error) {
// use `data` and `error` here
}];
// but not here
Note, in addition to adopting asynchronous completion block pattern, a few other observations:
If you’re going to create a new NSURLSession for each request, make sure to invalidate it or else you will leak memory.
I’m returning the NSURLSessionTask, which some callers may want in case they might want to cancel the request (e.g. if the view in question is dismissed or a new request must be generated). But as shown above, you don’t need to use this NSURLSessionTask reference if you don’t want.
I'm dispatching the completion handler back to the main queue. That is not strictly necessary, but it is often a useful convenience.
I have a method that returns a string usually locally, but with a backup from the Web. I was retrieving some JSON using dataWithContentsOfUrl but want to switch to using a Session object which is better for the UI and also--if I am not mistaken--allows the server to set a sessionId on the phone, however, I'm struggling with the async issue.
With the old code, I just returned the JSON but I'm struggling with how to do this for the asynchronous result. I can't change the calling method which returns a string. What can I do with the asynchronous Api call to use the data that is retrieved?
async:
-(void)getAsyncAnswerFor:(NSString*) str {
NSString *surl = [NSString stringWithFormat: #"https://~.com//api.php?q=%#",str];
NSURL *url = [NSURL URLWithString:surl];
NSURLSessionDataTask *downloadTask = [[NSURLSession sharedSession]
dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//HOW DO I PASS THIS BACK TO THE CALLING METHOD OR IS THAT IMPOSSIBLE
}];
[downloadTask resume];
}
sync
-(NSString*)getAnswerFor:(NSString*) str {
NSError *error;
NSString *surl = [NSString stringWithFormat: #"https://~.com//api.php?q=%#",str];
NSData *data = [NSData dataWithContentsOfURL: [NSURL URLWithString:surl]];
NSMutableArray *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
//process JSON
if (error) {
return #"";
}
return #"processed JSON";
}
Would appreciate any suggestions.
If what I want to do is totally impossible, is it possible to set a sessionID on the phone without the Session object? I know setting a session ID is is not the greatest approach, but I'm trying to avoid a lot of authentication overhead.
You can pass a block to your asynchronous function and then call it when the url session completion handler is called. This is a trivial example:
- (void)doSomethingWithBlock:(void (^)(double, double))block {
...
block(21.0, 2.0);
}
I lifted this ^^ from the Apple Docs but you might be able to do something like this: (Note: I didn't check this in a compiler!)
-(void)getAsyncAnswerFor:(NSString*) str completion:(void (^)(NSData, NSURLResponse, NSError))block {
NSString *surl = [NSString stringWithFormat: #"https://~.com//api.php?q=%#",str];
NSURL *url = [NSURL URLWithString:surl];
NSURLSessionDataTask *downloadTask = [[NSURLSession sharedSession]
dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
block(data, response, error);
}];
[downloadTask resume];
}
You'll need to be careful if you try to reference self anywhere in the blocks.
I'm using blocks to get header fields from response in one class and I have to get that in another class.
I implemented code like this
In first class:
- (void)viewDidLoad {
[super viewDidLoad];
UserAuthentication *auth = [[UserAuthentication alloc]init];
NSDictionary *dict = [auth getUserConfiguration];
NSLog(#"%#",dict);
}
In userAuthentication class:
-(NSDictionary *)getUserConfiguration;
{
__block NSDictionary *resultDictionary;
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:#"http://72.52.65.142:8083/auth"]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if ([response respondsToSelector:#selector(allHeaderFields)]) {
resultDictionary = [httpResponse allHeaderFields];
NSLog(#"%#",resultDictionary);
}
}] resume];
NSLog(#"%#",resultDictionary);
return resultDictionary;
}
Here my problem is in first class I'm getting dict as null.
Even in userAuthentication class also I'm getting null.
But after some time call back method is calling and then I can see the response correctly in completionHandler.
So how I can get response in firstClass?
You are misunderstanding the basic principle of async operation that runs in background thread and when the operation is completed it gives you data in completion block.
To get response in viewDidLoad Method of second class you need to use blocks. like below
-(void)getUserConfigurationOnCompletion:(void (^)(NSDictionary *))completion
{
__block NSDictionary *resultDictionary;
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:#"http://72.52.65.142:8083/auth"]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if ([response respondsToSelector:#selector(allHeaderFields)]) {
resultDictionary = [httpResponse allHeaderFields];
// Call completion with parameter
completion(resultDictionary);
}
}] resume];
}
and use it like this in viewDidLoad
- (void)viewDidLoad {
[super viewDidLoad];
UserAuthentication *auth = [[UserAuthentication alloc]init];
[auth getUserConfigurationOnCompletion:^(NSDictionary *dict){
// do necessary work with response dictionary here
NSLog(#"%#",dict);
}];
}
That's something you'll have to get used to: Anything that is related to internet access (and some things not related to it) cannot be returned immediately - unless you are willing to wait for it, block your user interface, and make your users very, very unhappy.
You have to write your application in such a way that it can be in four states: Never asked for the user configuration, asking for the user configuration, having asked for and received the user configuration, or having asked for the user configuration and failed. In this case your view must handle all four possibilities and must handle when the situation changes.
You are using NSURLSession! It performs tasks on a background thread!
Completion block is called only when you get the response from the server. Naturally it will take time to complete the request. You should use blocks to complete the request and return the result on completion.
-(void)getUserConfigurationAndOnCompletion:(void(ˆ)(NSDictionary *dict, NSError *error))completion;
{
__block NSDictionary *resultDictionary;
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:#"http://72.52.65.142:8083/auth"]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if ([response respondsToSelector:#selector(allHeaderFields)]) {
resultDictionary = [httpResponse allHeaderFields];
NSLog(#"%#",resultDictionary);
//This will call the block in the first class with the result dictionary
dispatch_async(dispatch_get_main_queue(), ^{
if(!error){
completion(resultDictionary,nil);
}else{
completion(nil,error);
}
});
}] resume];
}
When you call the above code from your first class, it will create a block there and you will get the required dictionary over there in the block parameter!
Your method should be like,
-(void)getUserConfigurationwithCompletionHandler : (void (^)(NSDictionary* resultDictionary))completionHandler
{
__block NSDictionary *resultDictionary;
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:#"http://72.52.65.142:8083/auth"]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if ([response respondsToSelector:#selector(allHeaderFields)]) {
resultDictionary = [httpResponse allHeaderFields];
NSLog(#"%#",resultDictionary);
completionHandler(resultDictionary);
}
}] resume];
NSLog(#"%#",resultDictionary);
}
and you can access it like,
- (void)viewDidLoad {
[super viewDidLoad];
[self getUserConfigurationwithCompletionHandler:^(NSDictionary *resultDictionary) {
// you can acess result dictionary here
NSLog(#"%#",resultDictionary);
}];
}
because you will getting data in response of webservice(from server) so it takes some time to complete so you have to return data from completion handler of webservice call and you can't return data from completion handler so you have to create own completion handler and call as i have mentioned above. you can access resultDictionary in completionHandler and you can show new VC from this completionHandler.
You have to call a method in your first class in your completionHandler.
Create a property of type YOURFIRSTCLASS *myfirstclass in your UserAuthentication Class.
Pass your firstclass with "self" to the UserAuthentication object.
create visible method in your firstclass "-(void)responseCaller:(NSDictionary)dict"
call the method in your response method
YOURFIRSTCLASS .h:
-(void)responseCaller:(NSDictionary)dict;
YOURFIRSTCLASS .m
-(void)responseCaller:(NSDictionary)dict
{NSLog(#"%#",dict);}
- (void)viewDidLoad {
[super viewDidLoad];
UserAuthentication *auth = [[UserAuthentication alloc]init];
auth.myfirstclass = self;
NSDictionary *dict = [auth getUserConfiguration];
NSLog(#"%#",dict);
}
UserAuthentication .h
#import "YOURFIRSTCLASS.h"
#property (nonatomic) *myfirstclass;
UserAuthentication .m
-(NSDictionary *)getUserConfiguration;
{
__block NSDictionary *resultDictionary;
NSURLSession *session = [NSURLSession sharedSession];
__weak myfirstclassSave = myfirstclass;
[[session dataTaskWithURL:[NSURL URLWithString:#"http://72.52.65.142:8083/auth"]
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
if ([response respondsToSelector:#selector(allHeaderFields)]) {
resultDictionary = [httpResponse allHeaderFields];
[myfirstclassSave responseCaller:resultDictionary ];
}
}] resume];
return resultDictionary;
}
Something like that
I'm starting from scratch learning iOS programming.
I want my app to pull XML from a website. I'm thinking that to conform with the MVC pattern I should have a model class that simply provides a method to accomplish that (maybe have it parse the XML too and return an array).
Trouble is that all the tutorials I have found teach the NSURLSession in the context of the view and controller - so edit the appdelegate, or create a view controller, etc.
I got the following method from Apples documentation and I currently have it running as an IBAction when a button is pressed (so I can run it and test it easily). I'd like to get it working then put it in it's own class:
__block NSMutableData *webData;
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *delegateFreeSession = [NSURLSession sessionWithConfiguration:defaultConfigObject delegate: nil delegateQueue: [NSOperationQueue mainQueue]];
[[delegateFreeSession dataTaskWithURL: [NSURL URLWithString:url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
NSLog(#"Got response %# with error %#.\n", response, error);
NSLog(#"DATA:\n%#\nEND DATA\n", [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]);
webData = [[NSMutableData alloc] initWithData:data];
}
]resume];
My immediate question is:
Can someone explain how the completion handler is working and how to get data out of there? It's working, data is grabbing the xml from the website and logging it on the console, but copying it to webData doesn't work, it compiles but doesn't copy. (I'm still figuring out why the __block declaration allows webData to sneak in there in the first place!)
My bigger question would be if everyone thinks the idea of a separate model class for this process is a good idea. Is there a better way of designing this?
Thank you!
This may be just some confusion about how asynchronous blocks work. If you're doing this:
__block NSMutableData *webData;
// ...
[[delegateFreeSession dataTaskWithURL: [NSURL URLWithString:url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
NSLog(#"within the block, did I get data: %#", data);
webData = [[NSMutableData alloc] initWithData:data];
}]resume];
NSLog(#"after the block, did I get data: %#", webData);
You might be seeing output that looks like this:
after the block, did I get data: (null)
within the block, did I get data: <NSData ...
What gives? Why did the code after the block run first? And where was the data? The problem is with our definition of "after". The NSLog that appears after the block actually runs before the block runs. It runs as soon as the dataRequest is started. The code inside the block runs after the request has finished.
Keeping the data result in a block variable local to that method does you no good. The value is uninitialized when you hit the end of the method. The block initializes it when it the block runs, but the value is discarded as soon as the block finishes.
Fix: do your handling of the data within the block. Don't expect it to be valid until after the block runs (which is well after the method runs):
EDIT - It's 100% fine to use self inside this block to call methods, set properties, etc. You need to watch out for retain cycles only when the block itself is a property of self (or a property of something self retains), which it isn't...
// don't do this
//__block NSMutableData *webData;
// ...
[[delegateFreeSession dataTaskWithURL: [NSURL URLWithString:url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
NSLog(#"within the block, did I get data: %#", data);
NSMutableData *webData = [[NSMutableData alloc] initWithData:data];
// do whatever you plan to do with web data
// write it to disk, or save it in a property of this class
// update the UI to say the request is done
[self callAMethod:data]; // fine
[self callAnotherMethod]; // fine
self.property = data; // fine
}]resume];
// don't do this, there's no data yet
//NSLog(#"after the block, did I get data: %#", webData);
I'm relatively new to iOS development but I'm working on an application to get a better understanding of development. I'm working with a web service and want to check the credentials a user enters. To do this I am making a simple get request with their credentials and then checking the http status for 200. Here is my code below:
-(BOOL)checkCredentials:(NSString *)username withPassword:(NSString *)password{
NSString *requestString = #"SOME URL";
NSURL *url = [NSURL URLWithString:requestString];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
NSData *userPasswordData = [[NSString stringWithFormat:#"%#:%#", username, password] dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64EncodedCredential = [userPasswordData base64EncodedStringWithOptions:0];
NSString *authString = [NSString stringWithFormat:#"Basic %#", base64EncodedCredential];
NSURLSessionConfiguration *sessionConfig=[NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.HTTPAdditionalHeaders=#{#"Authorization":authString};
self.session=[NSURLSession sessionWithConfiguration:sessionConfig];
__block BOOL success = NO;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(!error){
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
success = YES;
}
}
NSMutableDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonObject);
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
[dataTask resume];
return success;
}
I was going to use a semaphore to wait for the block to complete so I can check the status code and then return. But first it seems like my code just hangs, and I think that because I don't have a release, but that's not allowed with ARC. I'm not sure why it's hanging. Is there a better way to wait for the block to complete (without a semaphore) so I can return whether my credentials are valid?
Also is there a better way to pass the username and password so that it's not possible for someone to spoof the username and password?
Any help would be greatly appreciated.
Think simple!
Make your own completionHandler so that you won't deal with the return anymore, the caller will take the responsibility of result verification instead.
There's one thing you need to keep in mind, that if you want to modify anything related to UI (User Interface), you need to dispatch your completion block to main queue or you will get unexpected behavior, see more detail here.
Change your return type to void and add a completion block:
-(void)checkCredentials:(NSString *)username withPassword:(NSString *)password completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))myCompletion
{
NSString *requestString = #"http://google.com";
NSURL *url = [NSURL URLWithString:requestString];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// Here you return exactly what the NSURLSessionDataTask downloaded
// and pass it to the caller as an another completion block
myCompletion(data, response, error);
}];
[dataTask resume];
}
Caller's code, I assume that self is the caller:
[self checkCredentials:#"" withPassword:#"" completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(!error){
// Result verification's here
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
NSLog(#"SUCESS");
}
}
}];
You code stops waiting for a semaphore and [dataTask resume] is never executed.
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); <=== waits here
[dataTask resume]; <=== never reached
I'd suggest not using the semaphore here. Do the work in your block instead.
As to username/password. If you worry about spoofing then SSL layer on top of HTTP is the answer.
This is a really dangerous pattern, because this call is going to block until the network request completes. If this is on the main thread, your app will stop responding and the watchdog may kill you.
That warning aside, the reason the block doesn't complete is because the network task is never started. You trap on your semaphore before you call resume, so your task never runs. I would also, personally use a dispatch_group to do the waiting.
To make it better, you would need to rewrite it asynchronously. Basically have your app continue to function, maybe disable the inputs, until the call completes, then run a block to re-enable them, or show an error:
// Assume your login button and whatever are exposed as properties here
self.loginButton.enabled = NO;
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(!error){
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
success = YES;
}
}
NSMutableDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonObject);
// Need to be back on the main queue, the call is complete
self.loginButton.enabled = YES;
}];
[dataTask resume];
Or, just to keep it the way you have it, but resolve the immediate issue, re-order your trap so that it happens after the task resumes:
[dataTask resume];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); // might want to time out here instead of waiting forever
return success;