How to return NSData from NSURLSessionDataTask com

2019-02-09 17:26发布

问题:

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:

  1. 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?

  2. How can I return the data please?

回答1:

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.



回答2:

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 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];
   }