How to make one function that organizes multiple NSURLSessionDataTask (s)? [closed] - ios

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
This question appears to be off-topic because it lacks sufficient information to diagnose the problem. Describe your problem in more detail or include a minimal example in the question itself.
Closed 8 years ago.
Improve this question
After creating an NSURLSession, it appears that making multiple web queries with NSURLSessionDataTask can lead to some code writing redundancy, which I'd like to clean up. The following code is repeated multiple times:
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
}];
[task resume];
Could the task be initiated from inside a dedicated function? It would be easy to pass in the NSMutableURLRequest as a variable but what about the completion handler?
Theoretically you could pass in the completion handler as a block, right? The problem I ran into is that the callback variables (data & response) can't be referred to from outside the provided completion handler, so how can you write a block that refers to them?
Right now I have 3 or 4 separately written tasks going, and it looks like it may stay that way unless someone has an idea!
EDIT, SOLUTION:
The method definition is:
- (void) engage:(NSMutableURLRequest *)request with:(void (^)(NSData *, NSURLResponse *, NSError *))yourmom;
& The block literal that it takes looks like:
void (^yourmom)(NSData *, NSURLResponse *, NSError *) = ^(NSData *data, NSURLResponse *response, NSError *error) {
// stuff
};
Basically, the key thing I was missing was that I needed my block to accept variables so that I could refer to them by name without compiler errors. The above code creates a variable-accepting block which lets me get around the compiler warnings about out of scope variables. Also if the code
[self engage:request with:yourmom];
shows up somewhere we'll know where it came from.

If you're asking how can you pass the completion block to the utility method, you should just supply a block parameter to your method:
- (NSURLSessionDataTask *)startDataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))block
{
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:block];
[task resume];
return task;
}
Or, if there are portions of the completion block which you're repeating all the time, then go ahead and put that in your utility method, but then invoke the caller's block:
- (NSURLSessionDataTask *)startDataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))block
{
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
// do here stuff that I'll always do, such as logging errors, checking response codes, etc.
// when that's done, I can now invoke the caller's block
if (block) {
block(data, response, error);
}
}];
[task resume];
return task;
}

I'd say make a category on NSURLSession and do the following:
First typedef a completion block in the header file:
typedef void (^NSURLSessionDataTaskCompletionHandler) (NSData *data, NSURLResponse *response, NSError *error);
Then add a method to the header file:
- (NSURLSessionDataTask *)startTaskWithRequest:(NSURLRequest *)request completionHandler:(NSURLSessionDataTaskCompletionHandler)completionHandler;
Finally the body of the function in the implementation file:
- (NSURLSessionDataTask *)startTaskWithRequest:(NSURLRequest *)request completionHandler:(NSURLsessionDataTaskCompletionHandler)completionHandler
{
NSURLSessionDataTask *task = [self dataTaskWithRequest:request completionHandler:completionHandler];
[task resume];
return task;
}
Now you get to very easily do something like:
[session startTaskWithRequest:request completionHandler:(NSData *data, NSURLResponse *response, NSError *error){
// do stuff
}];

Related

iOS 14 crash zombie when use dispatch_semaphore

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.

iOS 9 : Get Value From NSURLSessionDataTask In Framework

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

How to call method with block in Object C?

I have declared method as:
- (void) authorizeUser:(OauthObject *) user withUsername: (NSString *) username withPassword: (NSString *) password completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error)) CallCompletion;
I try to call this method like as:
[[ManagerServerRequest sharedManagerServerRequest] authorizeUser:(OauthObject *) withUsername:self.login.text withPassword:self.login.text completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// TODO
}]
Where ManagerServerRequest sharedManagerServerRequest is singlton object.
I get error:
use undeclared identifier WithUsername
How to call this method correctly?
Review:
I pass oauthObj:
OauthObject* oauthObj = [[OauthObject alloc] init];
[[ManagerServerRequest sharedManagerServerRequest]
authorizeUser: oauthObj and:
withUsername:self.login.text
withPassword:self.login.text
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// TODO
}]
I can error
The problem is unrelated to block syntax. In your code
[[ManagerServerRequest sharedManagerServerRequest]
authorizeUser:(OauthObject *)
withUsername:self.login.text
withPassword:self.login.text
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// TODO
}]
You are missing the actual argument for authorizeUser:, replace (OauthObject *) with the actual object and you are fine.
The code doesn't compile, but that's what you said in your question anyway :-)
From the error message, it looks like you are using WithUsername when you should be using withUsername.

How to delay method until NSData finishes loading in NSUrlSession?

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.

Block not completing in iOS

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;

Resources