Sign up for a Parse account to implement this tutorial and more!

Sign Up

Icon_saving_images
Saving Images

Learn how to make an app that allows the user to take photos and upload it directly to Parse.

iOS
uploading
images
PFFile
PFObject

Download code for this tutorial:

.zip File GitHub

Photo apps are great. If you want to create a photo app of your own, Parse can handle the entire backend, including saving the files, retrieving them, and associating them with a user.

In this tutorial, we’ll show you how you can store your photos to the Parse platform, without having to worry about server-side coding.

The final product should look something like the images below. We recommend you check out the sample demo and play around with it first to get a better idea.

Grid view of all the imagesDetailed view of each image

Setup

We'll begin with a view-based project. Let us first add these items to ViewController.xib: a UINavigationBar, two UIBarButtonItems (camera/refresh), and a UIScrollView in the middle. Your final nib file should look like this:

ViewController.xib

Once done, create an IBOutlet that hooks up to the UIScrollView as well as two dummy IBAction methods that correspond to the camera button and refresh button respectively.

Ensure that you have your IBOutlets and IBActions hooked up correctly. You can check this by clicking on File’s Owner and opening the Connections Inspector (see screenshot).

Also, do remember to import the right frameworks. You should have: Parse.framework, AudioToolbox.framework, CFNetwork.framework, libz1.1.3.dylib, MobileCoreServices.framework, QuartzCore.framework, Security.framework, SystemConfiguration.framework, UIKit.framework, Foundation.framework, CoreGraphics.framework.

PhotoDetailViewController.xib

Finally, we will be using MBProgressHUD to display an activity indicator or spinner. Start by downloading MBProgressHUD, then adding both MBProgressHUD.h and MBProgressHUD.m to your Xcode project.

Creating Users

Each file that is uploaded to the cloud can be associated with a PFUser. In this tutorial, we are going to use a dummy username and password to sign up. You can learn more about PFUsers here: https://www.parse.com/docs/ios_guide#users.

Let’s insert this first piece of code in the app delegate and go through it after:

PFUser *currentUser = [PFUser currentUser];
    if (currentUser) {
        [self refresh:nil];
    } else {
        // Dummy username and password
        PFUser *user = [PFUser user];
        user.username = @"Matt";
        user.password = @"password";
        user.email = @"Matt@example.com";
        
        [user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
            if (!error) {
                [self refresh:nil];
            } else {
                [PFUser logInWithUsername:@"Matt" password:@"password"];
                [self refresh:nil];
            }
        }];
    }
}

This checks if there is a current user that is logged into the system. This could be a user that is already saved and cached on the system when they signed up or logged in previously. If there is no cached user on disk, we create one.

Again, you need to account for both scenarios: if the username exists, we just need to log him in.

Configuring the Header File

Before implementing anything, we should configure the header file. First import <Parse/Parse.h> into our ViewController.h so we can make references to any methods in them, include MBProgressHUD.h for our activity indicators, and include stdlib.h for a randomizer which we will use shortly. Also remember to adopt the protocols UINavigationControllerDelegate, UIImagePickerControllerDelegate and MBProgressHUDDelegate to use their delegate methods. The UINavigationControllerDelegate is adhered to because a UIImagePickerController is a subclass of UINavigationController.

Next, add the instance variables photoScrollView, allImages, HUD, and refreshHUD. The photoScrollView refers to the scroll view that shows the photos, allImages refers to an array that holds our photos, and the last two refer to the HUDs that we will be using shortly.

Finally, we have 5 methods that we will be using today. Go ahead and add them.

//ViewController.h

#import <UIKit/UIKit.h>
#import <Parse/Parse.h>
#import "MBProgressHUD.h"
#include <stdlib.h>

@interface ViewController : UIViewController <UINavigationControllerDelegate, UIImagePickerControllerDelegate, MBProgressHUDDelegate>
{
    IBOutlet UIScrollView *photoScrollView;
    NSMutableArray *allImages;
    
    MBProgressHUD *HUD;
    MBProgressHUD *refreshHUD;
}

- (IBAction)refresh:(id)sender;
- (IBAction)cameraButtonTapped:(id)sender;
- (void)uploadImage:(NSData *)imageData;
- (void)setUpImages:(NSArray *)images;
- (void)buttonTouched:(id)sender;

Implementing the HUD Delegate and Image Resizing Methods

We also need to implement the only HUD delegate method (add this in the implementation file):

- (void)hudWasHidden:(MBProgressHUD *)hud {
    // Remove HUD from screen when the HUD hides
    [HUD removeFromSuperview];
    HUD = nil;
}

This allows us to remove the HUD from the main view when the time is right.

Using the UIImagePickerController

We shall be uploading photos from the device itself. If it possesses a camera, then it will take a picture and upload it from there. If it does not, it will upload an existing image from the device or simulator. As of now, there are 5 included images which are licensed (feel free to use them as long as you attribute them as well).

The first step is to create a UIImagePickerController in the method cameraButtonTapped. We first verify that the camera source is available, and then we create the UIImagePickerController object. Remember to set the source type and delegate.

if ([UIImagePickerController isSourceTypeAvailable:
UIImagePickerControllerSourceTypeCamera] == YES){
        // Create image picker controller
        UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
        
        // Set source to the camera
        imagePicker.sourceType =  UIImagePickerControllerSourceTypeCamera;
        
        // Delegate is self
        imagePicker.delegate = self;
        
        // Show image picker
        [self presentModalViewController:imagePicker animated:YES];
    }

Before taking care of non-camera devices, we should finish up the image picker delegate method. We resize and compress the returned JPEG and call the method uploadImage with the imageData passed in.

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    // Access the uncropped image from info dictionary
    UIImage *image = [info objectForKey:@"UIImagePickerControllerOriginalImage"];
    
    // Dismiss controller
    [picker dismissModalViewControllerAnimated:YES];
    
    // Resize image
    UIGraphicsBeginImageContext(CGSizeMake(640, 960));
    [image drawInRect: CGRectMake(0, 0, 640, 960)];
    UIImage *smallImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();   

    // Upload image
    NSData *imageData = UIImageJPEGRepresentation(image, 0.05f);
    [self uploadImage:imageData];
}

Next we need to account for devices without a camera, or the simulator. We randomly pick an existing UIImage, resize and crunch it into NSData with a high compression quality, and pass the imageData our uploadImage method as well.

else{
    // Device has no camera
    UIImage *image;
    int r = arc4random() % 5;
    switch (r) {
        case 0:
            image = [UIImage imageNamed:@"ParseLogo.jpg"];
            break;
        case 1:
            image = [UIImage imageNamed:@"Crowd.jpg"];
            break;
        case 2:
            image = [UIImage imageNamed:@"Desert.jpg"];
            break;
        case 3:
            image = [UIImage imageNamed:@"Lime.jpg"];
            break;
        case 4:
            image = [UIImage imageNamed:@"Sunflowers.jpg"];
            break;
        default:
            break;
    }

    // Resize image
    UIGraphicsBeginImageContext(CGSizeMake(640, 960));
    [image drawInRect: CGRectMake(0, 0, 640, 960)];
    UIImage *smallImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();   
        
    NSData *imageData = UIImageJPEGRepresentation(image, 0.05f);
    [self uploadImage:imageData];
}

Uploading Photos

We'll be associating PFUsers with a PFObject that has a PFFile as an attribute. Once we do that, we save the PFObject to the cloud. Remember we can always catch all the errors in the block if you want to do more customization.

We can update the HUD's progress in the second block with the parameter percentDone.

In our uploadImage method, we implement it as such:

PFFile *imageFile = [PFFile fileWithName:@"Image.jpg" data:imageData];
    
//HUD creation here (see example for code)
    
// Save PFFile
[imageFile saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    if (!error) {
        // Hide old HUD, show completed HUD (see example for code)
            
        // Create a PFObject around a PFFile and associate it with the current user
        PFObject *userPhoto = [PFObject objectWithClassName:@"UserPhoto"];
        [userPhoto setObject:imageFile forKey:@"imageFile"];
            
        // Set the access control list to current user for security purposes
        userPhoto.ACL = [PFACL ACLWithUser:[PFUser currentUser]];
            
        PFUser *user = [PFUser currentUser];
        [userPhoto setObject:user forKey:@"user"];
            
        [userPhoto saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
            if (!error) {
                [self refresh:nil];
            }
            else{
                // Log details of the failure
                NSLog(@"Error: %@ %@", error, [error userInfo]);
            }
        }];
    }
    else{
        [HUD hide:YES];
        // Log details of the failure
            NSLog(@"Error: %@ %@", error, [error userInfo]);
        }
} progressBlock:^(int percentDone) {
    // Update your progress spinner here. percentDone will be between 0 and 100.
    HUD.progress = (float)percentDone/100;
}];

After saving the PFObject, we can call the refresh method upon a successful callback, which will call the server and download the images.

Refreshing the Grid View

Now in ViewController.m, we need to implement the refresh IBAction method.

First off, we will create a separate HUD for refreshing so that it does not interfere with our other HUD which we used previously for uploading.

refreshHUD = [[MBProgressHUD alloc] initWithView:self.view];
[self.view addSubview:refreshHUD];
    
// Register for HUD callbacks so we can remove it from the window at the right time
refreshHUD.delegate = self;
    
// Show the HUD while the provided method executes in a new thread
[refreshHUD show:YES];

Creating the Download Query

Here's where the bulk of the code is, so we'll do it step by step.

To download images, we need to use PFQuery. Queries are used to retrieve objects from Parse, with conditions to narrow your search.

In order to extract all our uploaded images, we need to create a PFQuery that corresponds to the class name we previously set. Furthermore, we need to limit the retrieved results to those that belong to the current user.

Once the query has been created, we can begin retrieving the results by calling findObjectsInBackgroundWithBlock: method.

- (void)downloadAllImages
{
    PFQuery *query = [PFQuery queryWithClassName:@"UserPhoto"];
    PFUser *user = [PFUser currentUser];
    [query whereKey:@"user" equalTo:user];
    [query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    // If there are photos, we start extracting the data
    // Save a list of object IDs while extracting this data
        
    NSMutableArray *newObjectIDArray = [NSMutableArray array];
    NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
        
    if (objects.count > 0) {
        for (PFObject *eachObject in objects) {
            [newObjectIDArray addObject:[eachObject objectId]];
        }
    }

To make downloads faster, we will be saving the objectIDs of each photo in NSUserDefaults. When the query result is returned, we only download the photos with new objectIDs.

For the missing objectIDs, we will be deleting them from the photoScrollView. This occurs when an entry in the database has been removed, e.g. removing from Parse's web interface. To delete them, we check for the tag on each button (which will be shown to you how we set it up later).

// Compare the old and new object IDs
NSMutableArray *newCompareObjectIDArray = [NSMutableArray arrayWithArray:newObjectIDArray];
NSMutableArray *newCompareObjectIDArray2 = [NSMutableArray arrayWithArray:newObjectIDArray];
NSMutableArray *oldCompareObjectIDArray = [NSMutableArray arrayWithArray:[standardUserDefaults objectForKey:@"objectIDArray"]];
if ([standardUserDefaults objectForKey:@"objectIDArray"]){
    [newCompareObjectIDArray removeObjectsInArray:oldCompareObjectIDArray]; // New objects
            
    // Remove old objects if you delete them using the web browser
    [oldCompareObjectIDArray removeObjectsInArray:newCompareObjectIDArray2];
    if (oldCompareObjectIDArray.count > 0){
        // Check the position in the objectIDArray and remove
        NSMutableArray *listOfToRemove = [[NSMutableArray alloc] init];
        for (NSString *objectID in oldCompareObjectIDArray){
            int i = 0;
            for (NSString *oldObjectID in [standardUserDefaults objectForKey:@"objectIDArray"]){
                if ([objectID isEqualToString:oldObjectID]){
                    // Remove it
                    for (UIView *view in [photoScrollView subviews]) {
                        if ([view isKindOfClass:[UIButton class]]) {
                            if (view.tag == i){
                                [view removeFromSuperview];
                                NSLog(@"Removing picture at position %i",i);
                            }
                        }
                    }
                            
                    // Make list of all that you want to remove and remove at the end
                    [listOfToRemove addObject:[NSNumber numberWithInt:i]];
                }
                i++;
            }
        }

We remove it from the back so that the images will not skip.

// Remove from the back
NSSortDescriptor *highestToLowest = [NSSortDescriptor sortDescriptorWithKey:@"self" ascending:NO];
[listOfToRemove sortUsingDescriptors:[NSArray arrayWithObject:highestToLowest]];
                
for (NSNumber *index in listOfToRemove){                        
    [allImages removeObjectAtIndex:[index intValue]];
    [self setUpImages:allImages];     
}

Besides removing the missing objects, we also need to add the new objects. Converting backwards is easy. We go through each PFObject returned in the result and call getData on it to convert it to NSData. Don't forget to save the objectIDArray into the NSUserDefaults so we can load it up again on the next load.

// This method sets up the downloaded images and places them nicely in a grid
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        NSMutableArray *imageDataArray = [NSMutableArray array];
        
        // Iterate over all images and get the data from the PFFile
        for (int i = 0; i < images.count; i++) {
            PFObject *eachObject = [images objectAtIndex:i];
            PFFile *theImage = [eachObject objectForKey:@"imageFile"];
            NSData *imageData = [theImage getData];
            UIImage *image = [UIImage imageWithData:imageData];
            [imageDataArray addObject:image];
        }
                   
        // Dispatch to main thread to update the UI
        dispatch_async(dispatch_get_main_queue(), ^{
            // Remove old grid
            for (UIView *view in [photoScrollView subviews]) {
                if ([view isKindOfClass:[UIButton class]]) {
                    [view removeFromSuperview];
                }
            }
            
            // Create the buttons necessary for each image in the grid
            for (int i = 0; i < [imageDataArray count]; i++) {
                PFObject *eachObject = [images objectAtIndex:i];
                UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
                UIImage *image = [imageDataArray objectAtIndex:i];
                [button setImage:image forState:UIControlStateNormal];
                button.showsTouchWhenHighlighted = YES;
                [button addTarget:self action:@selector(buttonTouched:) forControlEvents:UIControlEventTouchUpInside];
                button.tag = i;
                button.frame = CGRectMake(THUMBNAIL_WIDTH * (i % THUMBNAIL_COLS) + PADDING * (i % THUMBNAIL_COLS) + PADDING,
                                          THUMBNAIL_HEIGHT * (i / THUMBNAIL_COLS) + PADDING * (i / THUMBNAIL_COLS) + PADDING + PADDING_TOP,
                                          THUMBNAIL_WIDTH,
                                          THUMBNAIL_HEIGHT);
                button.imageView.contentMode = UIViewContentModeScaleAspectFill;
                [button setTitle:[eachObject objectId] forState:UIControlStateReserved];
                [photoScrollView addSubview:button];
            }
            
            // Size the grid accordingly
            int rows = images.count / THUMBNAIL_COLS;
            if (((float)images.count / THUMBNAIL_COLS) - rows != 0) {
                rows++;
            }
            int height = THUMBNAIL_HEIGHT * rows + PADDING * rows + PADDING + PADDING_TOP;
            
            photoScrollView.contentSize = CGSizeMake(self.view.frame.size.width, height);
            photoScrollView.clipsToBounds = YES;
        });
    });

In this case, we will make use of Grand Central Dispatch to handle our getData. To do so, we create a dispatch queue and perform an asynchronous call. Once done, we add it to the overall photoArray (this manages all our photos), and then call setUpImages to arrange our images neatly in the main thread.

Setting up the Grid

In the setUpImages method, we copy over the images to allImages to ensure that our images are saved. After which, we remove all current buttons and replace them with new ones.

allImages = [images mutableCopy];
    
// Remove old grid
for (UIView *view in [photoScrollView subviews]) {
    if ([view isKindOfClass:[UIButton class]]) {
        [view removeFromSuperview];
    }
}

For each image, we create a UIButton to place in the grid. We also tag each UIButton so that we have a reference to the UIButton that would be tapped later. Also, we hook each UIButton up to a target method that opens up our detail view controller to show the image.

Finally we calculate the right contentSize for photoScrollView and set clipsToBounds to YES. This sets up the grid properly. The padding between images are 4px, while the thumbnail size is 75px by 75px, and the number of columns are 4.

// This method sets up the downloaded images and places them nicely in a grid
UIButton *button;
UIImage *thumbnail;

//Create a button for each image
for (int i=0; i<images.count; i++) {
    thumbnail = [images objectAtIndex:i];       
    button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setImage:thumbnail forState:UIControlStateNormal];
    button.showsTouchWhenHighlighted = YES;
    [button addTarget:self action:@selector(buttonTouched:) forControlEvents:UIControlEventTouchUpInside];
    button.tag = i;
    button.frame = CGRectMake(THUMBNAIL_WIDTH * (i % THUMBNAIL_COLS) + PADDING * (i % THUMBNAIL_COLS) + PADDING, THUMBNAIL_HEIGHT * (i / THUMBNAIL_COLS) + PADDING * (i / THUMBNAIL_COLS) + PADDING + PADDING_TOP, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
    button.imageView.contentMode = UIViewContentModeScaleAspectFill;
    [photoScrollView addSubview:button];
}
    
int rows = images.count / THUMBNAIL_COLS;
if (((float)images.count / THUMBNAIL_COLS) - rows != 0) {
    rows++;
}

int height = THUMBNAIL_HEIGHT * rows + PADDING * rows + PADDING + PADDING_TOP;
    
photoScrollView.contentSize = CGSizeMake(self.view.frame.size.width, height);
photoScrollView.clipsToBounds = YES;

Opening a Detail View Controller

We can open up the picture in detail in a separate modal view controller. Here’s how we can do it.

First create a new UIViewController (File > New > New File > UIViewController subclass), naming it PhotoDetailViewController. Once done, remember to set up the nib and import “PhotoDetailViewController.h” at the top of ViewController.m.

The nib should look like this, with a UINavigationBar, a UIBarButtonItem (that closes the controller; remember to create a IBAction method for this!), and a UIImageView that displays the image in detail:

PhotoDetailViewController.xib

Since we have a tag corresponding to each UIButton, we can get the exact picture in the allImages array we saved earlier on. With this image, we pass it on to the new detail view controller and present the controller.

- (void)buttonTouched:(id)sender{
    //When picture is touched, open a viewcontroller with the image
    UIImage *selectedPhoto = [allImages objectAtIndex:[sender tag]];
    
    PhotoDetailViewController *pdvc = [[PhotoDetailViewController alloc] init];
    
    pdvc.selectedImage = selectedPhoto;
    [self presentModalViewController:pdvc animated:YES];
}

In this tutorial, we have covered uploading photos to the cloud by wrapping the files in PFObjects, downloading them using PFQueries, and placing them in a grid. Also, we did some simple caching to only download the new images/remove missing ones, as well as creating a detailed view controller.