18
Mar
2013
 

Anonymous Image File Upload in iOS With Imgur

by Matt Long

It seems like a pretty useful feature, anonymous image file upload in iOS with imgur. If you need to upload images and don’t want to fool with some authentication mess like OAuth this technique is perfect. There are libraries that have simplified the OAuth process, however, it’s nice to be able to just initiate an upload, get a result URL back and be on your way. No fuss, no muss. That’s what I was looking for, so I wrote a little app that demonstrates how to do exactly that.

Imgur and You

There are numerous image upload services out there, but imgur seems to be the simplest to implement. Now, that being said, it’s pretty difficult to find any sample code out there that is up to date and works with their v3 API. I found a library that is several years old and only works with the v2 API, but that seemed odd to me considering how long their v3 API has been around. This means either developers don’t have a need for this kind of service or that getting it to work is too obscure and/or difficult. The bottom line for me was realizing that the API is expecting a multi-part form with properly formatted data parameters. Once I figured that out, it was pretty straight forward. I wrote a little demo app you can get on github that shows how to do it.

The first thing you need to do, though, is head over to https://api.imgur.com/oauth2/addclient and register your app. A client ID will be generated and emailed to you. You can then plug it in and start posting images.

The Basic Implementation

All you need to do is call a class method on my MLIMGURUploader class and initiate your upload with an NSData object and your client ID.

- (IBAction)didTapUploadButton:(id)sender
{
  NSString *clientID = @"YOUR_CLIENT_ID_HERE";

  NSString *title = [[self titleTextField] text];
  NSString *description = [[self descriptionTextField] text];

  [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
  
  __weak MLViewController *weakSelf = self;
  // Load the image data up in the background so we don't block the UI
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    UIImage *image = [UIImage imageNamed:@"balloons.jpg"];
    NSData *imageData = UIImageJPEGRepresentation(image, 1.0f);
    
    [MLIMGURUploader uploadPhoto:imageData 
                           title:title 
                     description:description 
                   imgurClientID:clientID completionBlock:^(NSString *result) {
      dispatch_async(dispatch_get_main_queue(), ^{
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
        [[weakSelf linkTextView] setText:result];
      });
    } failureBlock:^(NSURLResponse *response, NSError *error, NSInteger status) {
      dispatch_async(dispatch_get_main_queue(), ^{
      [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
      [[[UIAlertView alloc] initWithTitle:@"Upload Failed"
                                  message:[NSString stringWithFormat:@"%@ (Status code %d)", 
                                                   [error localizedDescription], status]
                                 delegate:nil
                        cancelButtonTitle:nil
                        otherButtonTitles:@"OK", nil] show];
      });
    }];
    
  });
}

This demo app just takes an NSData containing the image that we just added to the project (retrieved with a call to [UIImage imageNamed:]), a title, and a description and passes them on to the uploader.

Anonymous Image File Upload in iOS With Imgur:IMGUR Uploader

When the request returns, you receive back an NSString containing the URL to the newly created image.

[MLIMGURUploader uploadPhoto:imageData 
                       title:title 
                 description:description 
               imgurClientID:clientID completionBlock:^(NSString *result) {
  // The variable "result" contains the URL pointing to the raw image

} failureBlock:^(NSURLResponse *response, NSError *error, NSInteger status) {
  // Analyze the error. Something went wrong.

}];

Code Purity

It’s often difficult to achieve code purity where you’re only using built in libraries to develop your apps, but over the years I’ve become much more of a purist and try to eliminate the need for libraries as much as possible. I haven’t always been that way. Shortcuts seem like a good idea when you’re in a hurry to get something done, but in the end they often just end up biting you.

With this code purity idea in mind, here is the result for the imgur uploader–all in a single class method that only uses built in foundation libraries:

+ (void)uploadPhoto:(NSData*)imageData
              title:(NSString*)title
        description:(NSString*)description
      imgurClientID:(NSString*)clientID
    completionBlock:(void(^)(NSString* result))completion
       failureBlock:(void(^)(NSURLResponse *response, NSError *error, NSInteger status))failureBlock
{
  NSAssert(imageData, @"Image data is required");
  NSAssert(clientID, @"Client ID is required");
  
  NSString *urlString = @"https://api.imgur.com/3/upload.json";
  NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init] ;
  [request setURL:[NSURL URLWithString:urlString]];
  [request setHTTPMethod:@"POST"];
  
  NSMutableData *requestBody = [[NSMutableData alloc] init];
  
  NSString *boundary = @"---------------------------0983745982375409872438752038475287";
  
  NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
  [request addValue:contentType forHTTPHeaderField:@"Content-Type"];
  
  // Add client ID as authrorization header
  [request addValue:[NSString stringWithFormat:@"Client-ID %@", clientID] forHTTPHeaderField:@"Authorization"];
  
  // Image File Data
  [requestBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
  [requestBody appendData:[@"Content-Disposition: attachment; name=\"image\"; filename=\".tiff\"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
  
  [requestBody appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
  [requestBody appendData:[NSData dataWithData:imageData]];
  [requestBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
  
  // Title parameter
  if (title) {
    [requestBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [requestBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"title\"\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
    [requestBody appendData:[title dataUsingEncoding:NSUTF8StringEncoding]];
    [requestBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
  }
  
  // Description parameter
  if (description) {
    [requestBody appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [requestBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"description\"\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
    [requestBody appendData:[description dataUsingEncoding:NSUTF8StringEncoding]];
    [requestBody appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
  }
  
  [requestBody appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
  
  [request setHTTPBody:requestBody];
  
  [NSURLConnection sendAsynchronousRequest:request 
                                     queue:[NSOperationQueue mainQueue] 
                         completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
    NSDictionary *responseDictionary = [NSJSONSerialization JSONObjectWithData:data 
                                                                       options:NSJSONReadingMutableContainers 
                                                                         error:nil];
    if ([responseDictionary valueForKeyPath:@"data.error"]) {
      if (failureBlock) {
        if (!error) {
          // If no error has been provided, create one based on the response received from the server
          error = [NSError errorWithDomain:@"imguruploader" 
                                      code:10000 
                                  userInfo:@{NSLocalizedFailureReasonErrorKey : 
                                                 [responseDictionary valueForKeyPath:@"data.error"]}];
        }
        failureBlock(response, error, [[responseDictionary valueForKey:@"status"] intValue]);
      }
    } else {
      if (completion) {
        completion([responseDictionary valueForKeyPath:@"data.link"]);
      }
      
    }
    
  }];
}

Request Results

A successful request will provide a URL that points to the raw image you uploaded to the service. If you would prefer to access the imgur link that displays their site chrome and the title and description your provided in the upload, simply remove the file extension. So, for example, the url http://i.imgur.com/bQzUcIp.jpg will provide the raw image, the link http://i.imgur.com/bQzUcIp provides the imgur site wrapped version.

Conclusion

I’m glad that in the end this process of anonymously uploading an image file was pretty trivial. Though you never know with some of these services these days–they could change the way it works at any time. Until then, though, I hope it will help you as well. Until next time.

Get the project on GitHub