asynchronous upload with NSURLSession will not work but synchronous NSURLConnection does

后端 未结 1 946
栀梦
栀梦 2020-12-05 12:46

edit: I need to upload a file asynchronously from an iPhone to a Python server-side process. I\'d like to do the request asynchronously so that I can display a busy animatio

相关标签:
1条回答
  • 2020-12-05 12:46

    You are correct, that if you just want to make your request asynchronous, you should retire sendSynchronousRequest. While we once would have recommended sendAsynchronousRequest, effective iOS 9, NSURLConnection is formally deprecated and one should favor NSURLSession.

    Once you start using NSURLSession, you might find yourself drawn to it. For example, one can use a [NSURLSessionConfiguration backgroundSessionConfiguration:], then have uploads progress even after the app has gone into background. (You have to write a few delegate methods, so for simplicity's sake, I've stayed with a simple foreground upload below.) It's just a question of your business requirements, offsetting the new NSURLSession features versus the iOS 7+ limitation it entails.

    By the way, any conversation about network requests in iOS/MacOS is probably incomplete without a reference to AFNetworking. It greatly simplifies creation of these multipart requests and definitely merits investigation. They have NSURLSession support, too (but I haven't used their session wrappers, so can't speak to it). But AFNetworking is undoubtedly worthy of your consideration. You can enjoy some of the richness of the delegate-base API (e.g. progress updates, cancelable requests, dependencies between operations, etc.), offering far greater control that available with convenience methods (like sendSynchronousRequest), but without dragging you through the weeds of the delegate methods themselves.

    Regardless, if you're really interested in how to do uploads with NSURLSession, see below.


    If you want to upload via NSURLSession, it is a slight shift in thinking, namely, separating the configuration of the request (and its headers) in the NSMutableURLRequest from the creation of the the body of the request (which you now specify during the instantiation of the NSURLSessionUploadTask). The body of the request that you now specify as part of the upload task can be either a NSData, a file, or a stream (I use a NSData below, because we're building a multipart request):

    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"POST"];
    NSString *boundary = [self boundaryString];
    [request addValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary] forHTTPHeaderField:@"Content-Type"];
    
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
    
    NSData *fileData = [NSData dataWithContentsOfFile:path];
    NSData *data = [self createBodyWithBoundary:boundary username:@"rob" password:@"password" data:fileData filename:[path lastPathComponent]];
    
    NSURLSessionUploadTask *task = [session uploadTaskWithRequest:request fromData:data completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        NSAssert(!error, @"%s: uploadTaskWithRequest error: %@", __FUNCTION__, error);
    
        // parse and interpret the response `NSData` however is appropriate for your app
    }];
    [task resume];
    

    And the creation of the NSData being sent is much like your existing code:

    - (NSData *) createBodyWithBoundary:(NSString *)boundary username:(NSString*)username password:(NSString*)password data:(NSData*)data filename:(NSString *)filename
    {
        NSMutableData *body = [NSMutableData data];
    
        if (data) {
            //only send these methods when transferring data as well as username and password
            [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
            [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"file\"; filename=\"%@\"\r\n", filename] dataUsingEncoding:NSUTF8StringEncoding]];
            [body appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", [self mimeTypeForPath:filename]] dataUsingEncoding:NSUTF8StringEncoding]];
            [body appendData:data];
            [body appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
        }
    
        [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"username\"\r\n\r\n%@\r\n", username] dataUsingEncoding:NSUTF8StringEncoding]];
    
        [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
        [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"password\"\r\n\r\n%@\r\n", password] dataUsingEncoding:NSUTF8StringEncoding]];
    
        [body appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
    
        return body;
    }
    

    You hardcoded the boundary and the mime type, which is fine, but the above happens to use the following methods:

    - (NSString *)boundaryString
    {
        NSString *uuidStr = [[NSUUID UUID] UUIDString];
    
        // If you need to support iOS versions prior to 6, you can use
        // Core Foundation UUID functions to generate boundary string
        //
        // adapted from http://developer.apple.com/library/ios/#samplecode/SimpleURLConnections
        //
        // NSString  *uuidStr;
        //
        // CFUUIDRef uuid = CFUUIDCreate(NULL);
        // assert(uuid != NULL);
        // 
        // NSString  *uuidStr = CFBridgingRelease(CFUUIDCreateString(NULL, uuid));
        // assert(uuidStr != NULL);
        // 
        // CFRelease(uuid);
    
        return [NSString stringWithFormat:@"Boundary-%@", uuidStr];
    }
    
    - (NSString *)mimeTypeForPath:(NSString *)path
    {
        // get a mime type for an extension using MobileCoreServices.framework
    
        CFStringRef extension = (__bridge CFStringRef)[path pathExtension];
        CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, extension, NULL);
        assert(UTI != NULL);
    
        NSString *mimetype = CFBridgingRelease(UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType));
        assert(mimetype != NULL);
    
        CFRelease(UTI);
    
        return mimetype;
    }
    
    0 讨论(0)
提交回复
热议问题