objective-chttp-postmultipartform-datahttp-post-vars

POST multipart/form-data with Objective-C


So this HTML code submits the data in the correct format for me.

<form action="https://www.example.com/register.php" method="post" enctype="multipart/form-data">
    Name: <input type="text" name="userName"><BR />
    Email: <input type="text" name="userEmail"><BR />
    Password: <input type="text" name="userPassword"><BR />
    Avatar: <input type="file" name="avatar"><BR />
    <input type="submit">
</form>

I've looked into a good number of articles on how to do a multipart/form-data POST on iOS, but none really explain what to do if there were normal parameters as well as the file upload.

Could you please help me with the code to POST this in Obj-C?

Thanks!


Solution

  • The process is as follows:

    1. Create dictionary with the userName, userEmail, and userPassword parameters.

      NSDictionary *params = @{@"userName"     : @"rob",
                               @"userEmail"    : @"rob@email.com",
                               @"userPassword" : @"password"};
      
    2. Determine the path for the image:

      NSString *path = [[NSBundle mainBundle] pathForResource:@"avatar" ofType:@"png"];
      
    3. Create the request:

      NSString *boundary = [self generateBoundaryString];
      
      // configure the request
      
      NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
      [request setHTTPMethod:@"POST"];
      
      // set content type
      
      NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
      [request setValue:contentType forHTTPHeaderField: @"Content-Type"];
      
      // create body
      
      NSData *httpBody = [self createBodyWithBoundary:boundary parameters:params paths:@[path] fieldName:fieldName];
      
    4. This is the method used above to build the body of the request:

      - (NSData *)createBodyWithBoundary:(NSString *)boundary
                              parameters:(NSDictionary *)parameters
                                   paths:(NSArray *)paths
                               fieldName:(NSString *)fieldName {
          NSMutableData *httpBody = [NSMutableData data];
      
          // add params (all params are strings)
      
          [parameters enumerateKeysAndObjectsUsingBlock:^(NSString *parameterKey, NSString *parameterValue, BOOL *stop) {
              [httpBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
              [httpBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", parameterKey] dataUsingEncoding:NSUTF8StringEncoding]];
              [httpBody appendData:[[NSString stringWithFormat:@"%@\r\n", parameterValue] dataUsingEncoding:NSUTF8StringEncoding]];
          }];
      
          // add image data
      
          for (NSString *path in paths) {
              NSString *filename  = [path lastPathComponent];
              NSData   *data      = [NSData dataWithContentsOfFile:path];
              NSString *mimetype  = [self mimeTypeForPath:path];
      
              [httpBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
              [httpBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fieldName, filename] dataUsingEncoding:NSUTF8StringEncoding]];
              [httpBody appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", mimetype] dataUsingEncoding:NSUTF8StringEncoding]];
              [httpBody appendData:data];
              [httpBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
          }
      
          [httpBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
      
          return httpBody;
      }
      
    5. The above uses the following utility methods:

      @import MobileCoreServices;    // only needed in iOS
      
      - (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;
      }
      
      - (NSString *)generateBoundaryString {
          return [NSString stringWithFormat:@"Boundary-%@", [[NSUUID UUID] UUIDString]];
      }
      
    6. Then submit the request. There are many, many options here.

      For example, if using NSURLSession, you could create NSURLSessionUploadTask:

      NSURLSession *session = [NSURLSession sharedSession];  // use sharedSession or create your own
      
      NSURLSessionTask *task = [session uploadTaskWithRequest:request fromData:httpBody completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
          if (error) {
              NSLog(@"error = %@", error);
              return;
          }
      
          NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
          NSLog(@"result = %@", result);
      }];
      [task resume];
      

      Or you could create a NSURLSessionDataTask:

      request.HTTPBody = httpBody;
      
      NSURLSessionTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
          if (error) {
              NSLog(@"error = %@", error);
              return;
          }
      
          NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
          NSLog(@"result = %@", result);
      }];
      [task resume];
      

      The above assumes that the server is just returning text response. It's better if the server returned JSON, in which case you'd use NSJSONSerialization rather than NSString method initWithData.

      Likewise, I'm using the completion block renditions of NSURLSession above, but feel free to use the richer delegate-based renditions, too. But that seems beyond the scope of this question, so I'll leave that to you.

    But hopefully this illustrates the idea.


    I'd be remiss if I didn't point that, much easier than the above, you can use AFNetworking, repeating steps 1 and 2 above, but then just calling:

    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.responseSerializer = [AFHTTPResponseSerializer serializer]; // only needed if the server is not returning JSON; if web service returns JSON, remove this line
    NSURLSessionTask *task = [manager POST:urlString parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        NSError *error;
        if (![formData appendPartWithFileURL:[NSURL fileURLWithPath:path] name:@"avatar" fileName:[path lastPathComponent] mimeType:@"image/png" error:&error]) {
            NSLog(@"error appending part: %@", error);
        }
    }  progress:nil success:^(NSURLSessionTask *task, id responseObject) {
        NSLog(@"responseObject = %@", responseObject);
    } failure:^(NSURLSessionTask *task, NSError *error) {
        NSLog(@"error = %@", error);
    }];
    
    if (!task) {
        NSLog(@"Creation of task failed.");
    }