1. Code
  2. Mobile Development
  3. iOS Development

Enhancing a Photo App with GPUImage & iCarousel

Scroll to top

This tutorial will teach you how to use GPUImage to apply image filters in real-time as the device's camera feed is displayed. Along the way, you'll learn how to automatically populate images within a carousel controller and how to resize images with UIImage+Categories.


Project Overview


Tutorial Prerequisites

This tutorial builds on a previous post entitled "Build a Photo App with GPUImage". The previous lesson demonstrated how to use UIImagePickerController to select photos from the device's photo album or camera, how to add the GPUImage library to your project, and how to use the GPUImageFilter class to enhance still camera frames. If you're already familiar with UIImagePickerController and can figure out how to add GPUImage to your project on your own, you should be able to pick up from where the last tutorial left off just fine.


Step 1: Import iCarousel

This project will make extensive use of an open-source project called iCarousel in order to add stylish display of selected photos.

In order to include iCarousel in your project, go to the official GitHub page and download the source code as a zip file. Extract the code from the ZIP file and then drag-and-drop the folder titled "iCarousel" into the Xcode Project Navigator. This folder should contain both iCarousel.h and iCarousel.m. Be sure to select "Create groups for any added folders" and check the box next to "Copy items into destination group's folder (if needed)" as well as the box next to your project's target name in the "Add to targets" area.

Next go to ViewController.m and add an import statement for iCarousel:

1
2
#import "ViewController.h"

3
#import "GPUImage.h"

4
#import "iCarousel/iCarousel.h"

Step 2: Import UIImage+Categories

Before we display our images with iCarousel, we'll need to scale them down to an acceptable size. Rather than write all of the code to do this by hand, we'll make use of the excellent UIImage+Categories project, which provides basic functionality for resizing images as well as a few other image manipulation tricks.

Tip: You could alternatively use the MGImageUtilities project for this task. While the implementation details will differ slightly, it also provides excellent support for UIImage scaling.

Download the UIImage+Categories code from GitHub and then create a new group with the same name within Xcode. Drag both the implementation and header files for UIImage+Alpha, UIImage+Resize, and UIImage+RoundedCorner into your project. Be sure to select "Create groups for any added folders" and check the box next to "Copy items into destination group's folder (if needed)" as well as the box next to your project's target name in the "Add to targets" area.

Within the ViewController.m file, import the image categories with the following line of code:

1
2
#import "ViewController.h"

3
#import "GPUImage.h"

4
#import "iCarousel.h"

5
#import "UIImage+Resize.h"

Step 3: Add the iCarousel View in IB

With the iCarousel code imported into our project, switch over to the MainStoryboard.storyboard file to rework our interface.

First, select the current UIImageView connected to the selectedImageView IBOutlet and delete it. Switch back to ViewController.m and modify the project code to read as follows:

1
2
@property(nonatomic, weak) IBOutlet iCarousel *photoCarousel;
3
@property(nonatomic, weak) IBOutlet UIBarButtonItem *filterButton;
4
@property(nonatomic, weak) IBOutlet UIBarButtonItem *saveButton;
5
6
- (IBAction)photoFromAlbum;
7
- (IBAction)photoFromCamera;
8
- (IBAction)saveImageToAlbum;
9
- (IBAction)applyImageFilter:(id)sender;
10
11
@end
12
13
@implementation ViewController
14
15
@synthesize photoCarousel, filterButton, saveButton;

On line 1 above, replace the selectedImageView outlet with a iCarousel outlet called photoCarousel. Swap out the variables in the synthesize statement on line 14 above as well.

Go back to Interface Builder and drag a new UIView onto the view controller. With the new UIView selected, go to the "Identity inspector" tab within the Utilities pane and set the value for the "Class" field to "iCarousel". This tells Interface Builder that the UIView we added to the project should be instantiated as an instance of the iCarousel class.

Adding the iCarousel ViewAdding the iCarousel ViewAdding the iCarousel View

Now make a connection between the photoCarousel outlet just declared and the UIView just added as a subview.

Adding the iCarousel ViewAdding the iCarousel ViewAdding the iCarousel View

We need to set both the data source and delegate for photoCarousel as well, and we can achieve this from within Interface Builder. First, go to ViewController.h and declare that this view controller will conform to the appropriate protocols:

1
2
#import <UIKit/UIKit.h>

3
#import "iCarousel/iCarousel.h"

4
5
@interface ViewController : UIViewController <UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIActionSheetDelegate, iCarouselDataSource, iCarouselDelegate>

On line 2 we import iCarousel, and on line 4 we then declare conformance to both the delegate and the data source.

Back in the storyboard file, you can now map both the data source and the delegate to the view controller.

Adding the iCarousel ViewAdding the iCarousel ViewAdding the iCarousel View

Before moving on, go ahead and change the background color of the iCarousel view to black.

Okay, just one more thing. We want the iCarousel view to appear below the UIToolbar in the view hierarchy. You can do this visually by simply dragging them to the correct order in Interface Builder:

Adding the iCarousel View

Note how the iCarousel view now appears before the Toolbar.

Save your work in Interface Builder.


Step 4: Implement the iCarousel Protocols

iCarousel uses a design pattern similar to UITableView in that a data source is used to feed input into the control and a delegate is used to handle interaction with the control.

For our project, the data source will be a simple NSMutableArray called "displayImages". Add this to the class extension in ViewController.m now:

1
2
#import "UIImage+Resize.h"

3
4
@interface ViewController () 
5
{
6
    NSMutableArray *displayImages;
7
}
8
9
@property(nonatomic, weak) IBOutlet iCarousel *photoCarousel;

Next, we want to allocate memory for the array in the class' designated initializer. In our case, the view controller will be instantiated from a Storyboard, so the proper initializer is initWithCoder:. However, if the class were to be instantiated programmatically from a XIB, the proper initializer would be initWithNibName:bundle:. In order to accommodate either initialization style, we'll write our own custom initializer and call it from both, like so:

1
2
- (void)customSetup
3
{
4
    displayImages = [[NSMutableArray alloc] init];
5
}
6
7
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
8
{
9
    if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]))
10
    {
11
        [self customSetup];
12
    }
13
    return self;
14
}
15
16
- (id)initWithCoder:(NSCoder *)aDecoder
17
{
18
    if ((self = [super initWithCoder:aDecoder]))
19
    {
20
        [self customSetup];
21
    }
22
    return self;
23
}

Now we can implement the data source and delegate. Start with the data source method numberOfItemsInCarousel:, like so:

1
2
#pragma mark 

3
#pragma mark iCarousel DataSource/Delegate/Custom

4
5
- (NSUInteger)numberOfItemsInCarousel:(iCarousel *)carousel
6
{
7
    return [displayImages count];
8
}

This will tell iCarousel how many images to display by looking at the number of images stored in the data source array.

Next, write the method that will actually generate a view for each image displayed in the carousel:

1
2
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view
3
{    
4
    // Create new view if no view is available for recycling

5
    if (view == nil)
6
    {
7
        view = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 300.0f, 300.0f)];
8
        view.contentMode = UIViewContentModeCenter;
9
    }
10
11
   ((UIImageView *)view).image = [displayImages objectAtIndex:index];
12
    
13
    return view;
14
}

This is a good start, but there's one very significant issue with the above: the images should be scaled down before being supplied to iCarousel. Add the following lines of code to update the method:

1
2
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view
3
{    
4
    // Create new view if no view is available for recycling

5
    if (view == nil)
6
    {
7
        view = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 300.0f, 300.0f)];
8
        view.contentMode = UIViewContentModeCenter;
9
    }
10
    
11
    // Intelligently scale down to a max of 250px in width or height

12
    UIImage *originalImage = [displayImages objectAtIndex:index];
13
    
14
    CGSize maxSize = CGSizeMake(250.0f, 250.0f);
15
    CGSize targetSize;
16
    
17
    // If image is landscape, set width to 250px and dynamically figure out height

18
    if(originalImage.size.width >= originalImage.size.height)
19
    {
20
        float newHeightMultiplier = maxSize.width / originalImage.size.width;
21
        targetSize = CGSizeMake(maxSize.width, round(originalImage.size.height * newHeightMultiplier));
22
    } // If image is portrait, set height to 250px and dynamically figure out width

23
    else
24
    {
25
        float newWidthMultiplier = maxSize.height / originalImage.size.height;
26
        targetSize = CGSizeMake( round(newWidthMultiplier * originalImage.size.width), maxSize.height );
27
    }
28
    
29
    // Resize the source image down to fit nicely in iCarousel

30
    ((UIImageView *)view).image = [[displayImages objectAtIndex:index] resizedImage:targetSize interpolationQuality:kCGInterpolationHigh];
31
    
32
    return view;
33
}
Pro Tip: Using this method in a production app? Consider enhancing the code for performance by doing image resizing on a background thread and keeping a separate NSMutableArray that caches the scaled down image versions.
UPDATE 9/27/2012: Nick Lockwood (author of iCarousel) has released a project called FXImageView that will automatically handle image loading on a background thread. It also comes with other useful bells-and-whistles like drop shadows and rounded corners, so check it out!

Above, we set a maximum size of 250px for either the width or height of the image, and then we scale the opposite attribute down to match. This constrains the proportions of the image and looks much nicer than simply scaling down to a 250px by 250px square.

The above two methods are all iCarousel needs to start displaying images.

With the delegate and data source methods configured, now is a good time to setup the iCarousel object in the viewDidLoad method as well:

1
2
- (void)viewDidLoad
3
{
4
    [super viewDidLoad];
5
    
6
    // iCarousel Configuration

7
    self.photoCarousel.type = iCarouselTypeCoverFlow2;
8
    self.photoCarousel.bounces = NO;
9
}

With just a few more tweaks, the project will be able to display images within a carousel!


Step 5: Switch to the New Data Model

Earlier in this tutorial, we replaced the selectedImageView property with the photoCarousel property, updated the Storyboard interface to match, and created an NSMutableArray to act as the iCarousel data model. However, there are a few methods in ViewController.m still using the old data model that will prevent the project from compiling, so let's fix those now. Update the saveImageToAlbum method like so:

1
2
- (IBAction)saveImageToAlbum
3
{
4
    UIImage *selectedImage = [displayImages objectAtIndex:self.photoCarousel.currentItemIndex];
5
    UIImageWriteToSavedPhotosAlbum(selectedImage, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
6
}

Line 3 selects the UIImage from the data model that matches the current iCarousel index. Line 4 performs the actual disk write with that image.

Next, go to the UIImagePickerController delegate method and modify the code:

1
2
- (void)imagePickerController:(UIImagePickerController *)photoPicker didFinishPickingMediaWithInfo:(NSDictionary *)info
3
{
4
    self.saveButton.enabled = YES;
5
    self.filterButton.enabled = YES;
6
    
7
    [displayImages addObject:[info valueForKey:UIImagePickerControllerOriginalImage]];
8
    
9
    [self.photoCarousel reloadData];
10
        
11
    [photoPicker dismissViewControllerAnimated:YES completion:NULL];
12
    
13
}

On line 6 above, we add the selected photo to the new model and on line 8 we force a refresh of the carousel.

Just one more change to make. Go to the action sheet's clickedButtonAtIndex: method and modify the code as follows:

1
2
#pragma mark -

3
#pragma mark UIActionSheetDelegate

4
5
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
6
{
7
    
8
    if(buttonIndex == actionSheet.cancelButtonIndex)
9
    {
10
        return;
11
    }
12
    
13
    GPUImageFilter *selectedFilter;
14
    
15
    switch (buttonIndex) {
16
        case 0:
17
            selectedFilter = [[GPUImageGrayscaleFilter alloc] init];
18
            break;
19
        case 1:
20
            selectedFilter = [[GPUImageSepiaFilter alloc] init];
21
            break;
22
        case 2:
23
            selectedFilter = [[GPUImageSketchFilter alloc] init];
24
            break;
25
        case 3:
26
            selectedFilter = [[GPUImagePixellateFilter alloc] init];
27
            break;
28
        case 4:
29
            selectedFilter = [[GPUImageColorInvertFilter alloc] init];
30
            break;
31
        case 5:
32
            selectedFilter = [[GPUImageToonFilter alloc] init];
33
            break;
34
        case 6:
35
            selectedFilter = [[GPUImagePinchDistortionFilter alloc] init];
36
            break;
37
        case 7:
38
            selectedFilter = [[GPUImageFilter alloc] init];
39
            break;
40
        default:
41
            break;
42
    }
43
    
44
    UIImage *filteredImage = [selectedFilter imageByFilteringImage:[displayImages objectAtIndex:self.photoCarousel.currentItemIndex]];
45
    [displayImages replaceObjectAtIndex:self.photoCarousel.currentItemIndex withObject:filteredImage];
46
    [self.photoCarousel reloadData];
47
}

The final three lines of this method will filter the data model image that corresponds to the current carousel index, replace the carousel display with that image, and then refresh the carousel.

If all went well, you should now be able to compile and run the project! Doing so will allow you to view your images within the carousel instead of simply within an image view.


Step 6: Add a Gesture for Deleting Images

The app is looking good so far, but it would be nice if the user could remove a photo from the carousel after adding it to the display. No problem! We can select any UIGestureRecognizer subclass to make this happen. For this tutorial, I've chosen to use a two-finger double-tap. This gesture may not be immediately intuitive, but it is easy to perform and the added complexity will help prevent the removal of images accidentally.

Within the ViewController.m file, go to the carousel:viewForItemAtIndex:reusingView: method and add the following lines just before the end of method:

1
2
    // Resize the source image down to fit nicely in iCarousel

3
    ((UIImageView *)view).image = [[displayImages objectAtIndex:index] resizedImage:targetSize interpolationQuality:kCGInterpolationHigh];
4
    
5
    // Two finger double-tap will delete an image

6
    UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(removeImageFromCarousel:)];
7
    gesture.numberOfTouchesRequired = 2;
8
    gesture.numberOfTapsRequired = 2;
9
    view.gestureRecognizers = [NSArray arrayWithObject:gesture];
10
    
11
    return view;

Lines 4 - 8 declare a new UITapGestureRecognizer object, set the number of touches (i.e. fingers) required to trigger the gesture to 2, and set the number of taps required to 2 as well. Finally, just before passing the view back to the iCarousel object, we set the gestureRecognizers property with the newly formed recognizer.

Note that when triggered, this gesture recognizer will fire the selector removeImageFromCarousel:. Let's implement that next:

1
2
- (void)removeImageFromCarousel:(UIGestureRecognizer *)gesture
3
{
4
    [gesture removeTarget:self action:@selector(removeImageFromCarousel:)];
5
    [displayImages removeObjectAtIndex:self.photoCarousel.currentItemIndex];
6
    [self.photoCarousel reloadData];
7
}

Line 3 removes the gesture from the current target to prevent multiple gestures being triggered while processing. The remaining two lines are nothing new at this point.

Build and run the app again. You should now be able to dynamically remove items from the carousel!


Step 7: Create MTCameraViewController

The remainder of this tutorial will focus on using GPUImageStillCamera to build a custom camera picker control that can apply filters to the incoming video stream in real time. GPUImageStillCamera works closely with a class called GPUImageView. Camera frames generated by GPUImageStillCamera are sent to an assigned GPUImageView object for display to the user. All of this is accomplished with the underlying functionality provided by the AVFoundation framework, which provides programmatic access to camera frame data.

Because GPUImageView is a child class of UIView, we can embed the entire camera display into our own custom UIViewController class.

Add a new UIViewController subclass to the project by right clicking "PhotoFX" in the project navigator, and then selecting New File > Objective-C class. Name the class "MTCameraViewController" and enter "UIViewController" in for the "subclass of" field.

Creating MTCameraViewControllerCreating MTCameraViewControllerCreating MTCameraViewController

Click "Next" and then "Create" to complete the process.

Go to the MTCameraViewController.m file and import GPUImage:

1
2
#import "MTCameraViewController.h"

3
#import "GPUImage.h"

Next create a class extension with the necessary GPUImage data members:

1
2
@interface MTCameraViewController () <UIActionSheetDelegate>
3
{
4
    GPUImageStillCamera *stillCamera;
5
    GPUImageFilter *filter;
6
}
7
@end

Finally, go to the viewDidLoad: method and add the code to start up the camera capture:

1
2
- (void)viewDidLoad
3
{
4
    [super viewDidLoad];
5
6
    // Setup initial camera filter

7
    filter = [[GPUImageFilter alloc] init];
8
    [filter prepareForImageCapture];
9
    GPUImageView *filterView = (GPUImageView *)self.view;
10
    [filter addTarget:filterView];
11
12
    // Create custom GPUImage camera

13
    stillCamera = [[GPUImageStillCamera alloc] init];
14
    stillCamera.outputImageOrientation = UIInterfaceOrientationPortrait;
15
    [stillCamera addTarget:filter];
16
    
17
    // Begin showing video camera stream

18
    [stillCamera startCameraCapture];
19
    
20
}

Lines 5 - 9 create a new GPUImageView for displaying the camera feed and a default GPUImageFilter instance for applying a special effect to the view. We could have just as easily used one of the GPUImageFilter subclasses, such as GPUImageSketchFilter, but we'll instead start off with the default filter (i.e. no manipulations) and let the user dynamically select a filter later.

Lines 11 - 17 instantiate the GPU camera instance and apply the filter created previously to the camera before starting the capture.


Step 8: Add the Custom Camera in IB

Before the code from Step 8 will work, we need to add the custom MTCameraViewController class just created to the project's Storyboard.

Open the MainStoryboard.storyboard file and drag out a new View Controller from the Object library. With this object selected, go to the Identity inspector tab and set the "Class" field value to "MTCameraViewController".

Next, drag a UIToolbar onto the screen and set its style property to "Black Opaque" in the Attributes inspector. Then add two flexible width bar button items to the toolbar with a "Take Photo" UIBarButtonItem in the center.

Adding MTCameraViewController Subviews

To connect this view controller to the application flow, right click the "camera" button from the main view controller and drag the triggered segues outlet to the new view controller:

Adding New SegueAdding New SegueAdding New Segue

When prompted, select "Push" as the segue style.

With the newly added segue object still selected, go to the "Attributes inspector" and set the identifier to "pushMTCamera". Go ahead and make sure that "Push" is selected from the "Style" drop down.

Configuring SegueConfiguring SegueConfiguring Segue

With the segue created, ensure that the UIImagePicker will no longer be displayed when the user taps the camera button on the first app screen by disconnecting the IBAction outlet from the photoFromCamera method.

Finally, select the primary view of the newly created MTCameraViewController. Go to the Identity inspector and set the class value to "GPUImageView".

While not perfect just yet, if you build and run the app now, you should be able to push MTCameraViewController onto the view hierarchy and watch GPUImageView display the frames from the camera in real-time!


Step 9: Add Realtime Filter Selection

We can now add the logic necessary to control the filter applied to the camera display. First, go to the viewDidLoad: method within the MTCameraViewController.m file and add the code that will create a "Filter" button in the top right of the view controller:

1
2
- (void)viewDidLoad
3
{
4
    [super viewDidLoad];
5
6
    // Add Filter Button to Interface

7
    UIBarButtonItem *filterButton = [[UIBarButtonItem alloc] initWithTitle:@"Filter" style:UIBarButtonItemStylePlain target:self action:@selector(applyImageFilter:)];
8
    self.navigationItem.rightBarButtonItem = filterButton;

On line 6 above, we create a custom UIBarButtonItem that will trigger applyImageFilter: when selected.

Now create the selector method:

1
2
- (IBAction)applyImageFilter:(id)sender
3
{
4
    UIActionSheet *filterActionSheet = [[UIActionSheet alloc] initWithTitle:@"Select Filter"
5
                                                                   delegate:self
6
                                                          cancelButtonTitle:@"Cancel"
7
                                                     destructiveButtonTitle:nil
8
                                                          otherButtonTitles:@"Grayscale", @"Sepia", @"Sketch", @"Pixellate", @"Color Invert", @"Toon", @"Pinch Distort", @"None", nil];
9
    
10
    [filterActionSheet showFromBarButtonItem:sender animated:YES];
11
}

After adding the above you'll see a compiler warning stating that the current view controller doesn't conform to the UIActionSheetDelegate protocol. Fix that issue now by going to MTCameraViewController.h and modifying the class declaration like so:

1
2
#import <UIKit/UIKit.h>

3
4
@interface MTCameraViewController : UIViewController <UIActionSheetDelegate>
5
6
@end

Complete the circle by going back to the MTCameraViewController.m file and adding the logic that will respond to the UIActionSheet presented:

1
2
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
3
{
4
    // Bail if the cancel button was tapped

5
    if(actionSheet.cancelButtonIndex == buttonIndex)
6
    {
7
        return;
8
    }
9
    
10
    GPUImageFilter *selectedFilter;
11
    
12
    [stillCamera removeAllTargets];
13
    [filter removeAllTargets];
14
15
    
16
    switch (buttonIndex) {
17
        case 0:
18
            selectedFilter = [[GPUImageGrayscaleFilter alloc] init];
19
            break;
20
        case 1:
21
            selectedFilter = [[GPUImageSepiaFilter alloc] init];
22
            break;
23
        case 2:
24
            selectedFilter = [[GPUImageSketchFilter alloc] init];
25
            break;
26
        case 3:
27
            selectedFilter = [[GPUImagePixellateFilter alloc] init];
28
            break;
29
        case 4:
30
            selectedFilter = [[GPUImageColorInvertFilter alloc] init];
31
            break;
32
        case 5:
33
            selectedFilter = [[GPUImageToonFilter alloc] init];
34
            break;
35
        case 6:
36
            selectedFilter = [[GPUImagePinchDistortionFilter alloc] init];
37
            break;
38
        case 7:
39
            selectedFilter = [[GPUImageFilter alloc] init];
40
            break;
41
        default:
42
            break;
43
    }
44
        
45
    filter = selectedFilter;
46
    GPUImageView *filterView = (GPUImageView *)self.view;
47
    [filter addTarget:filterView];
48
    [stillCamera addTarget:filter];
49
    
50
}

Lines 11-12 are used to reset the currently selected filter.

Lines 15 - 42 above should look familiar to the logic in ViewController.m; we're just switching on the selected button to create an instance of the correlating filter.

Lines 44 - 47 take the newly created filter and apply it to the GPUImage camera.

If you build and run the project now, you should see that the newly created filter button allows the user to try out GPUImage filters in real time!


Step 10: Create a Camera Delegate Protocol

Now that we have the live feed filters working, the last major step in the tutorial is to allow the user to take snapshots with the GPUImage camera and then display them back in the main view controller's photo carousel.

In order to achieve this, we'll pass messages between view controllers using the delegation design pattern. Specifically, we'll create our own custom formal delegate protocol in MTCameraViewController and then configure the main ViewController class to conform to that protocol in order to receive delegation messages.

To get started, go to MTViewController.h and modify the code as follows:

1
2
#import <UIKit/UIKit.h>

3
4
@protocol MTCameraViewControllerDelegate
5
6
- (void)didSelectStillImage:(NSData *)image withError:(NSError *)error;
7
8
@end
9
10
@interface MTCameraViewController : UIViewController
11
12
@property(weak, nonatomic) id delegate;
13
14
@end

The above code declares a formal delegate pattern called MTCameraViewControllerDelegate on lines 3-7, and then creates a delegate object on line 11.

Next switch to MTCameraViewController.m and synthesize the delegate property:

1
2
@implementation MTCameraViewController 
3
4
@synthesize delegate;

With the protocol declared, we now need to implement it in the main ViewController class. Go to ViewController.h and add the following lines:

1
2
#import <UIKit/UIKit.h>

3
#import "iCarousel.h"

4
#import "MTCameraViewController.h"

5
6
@interface ViewController : UIViewController <UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIActionSheetDelegate, MTCameraViewControllerDelegate, iCarouselDataSource, iCarouselDelegate>
7
8
@end

Now open up the ViewController.m file. We want to assign the delegate property when the view controller is instantiated. Because we're using Storyboards, the proper place to do this is in the prepareForSegue:sender: method, which will be called just before the new view controller is pushed onto the screen:

1
2
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
3
{
4
    if([segue.identifier isEqualToString:@"pushMTCamera"])
5
    {
6
        // Set the delegate so this controller can received snapped photos

7
        MTCameraViewController *cameraViewController = (MTCameraViewController *) segue.destinationViewController;
8
        cameraViewController.delegate = self;
9
    }
10
}

Next we need to implement the didSelectStillImage:withError: method required by the MTCameraViewControllerDelegate protocol:

1
2
#pragma mark -

3
#pragma mark MTCameraViewController

4
5
// This delegate method is called after our custom camera class takes a photo

6
- (void)didSelectStillImage:(NSData *)imageData withError:(NSError *)error
7
{
8
    if(!error)
9
    {
10
        UIImage *image = [[UIImage alloc] initWithData:imageData];
11
        [displayImages addObject:image];
12
        [self.photoCarousel reloadData];
13
        
14
        self.filterButton.enabled = YES;
15
        self.saveButton.enabled = YES;
16
17
    }
18
    else
19
    {
20
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Capture Error" message:@"Unable to capture photo." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
21
        
22
        [alert show];
23
    }
24
}

The above code will convert the NSData object handed to the method to a UIImage and then reload the photo carousel.

Finally, we need to wrap things up by returning to MTCameraViewController.m and adding in the appropriate delegate method call. First, setup an IBAction method that will trigger a camera snap:

1
2
    GPUImageFilter *filter;
3
}
4
5
- (IBAction)captureImage:(id)sender;
6
7
@end

Before continuing, connect this method to the "Take Photo" button in the MainStoryboard.storyboard file.

Finally, add the method implementation:

1
2
-(IBAction)captureImage:(id)sender
3
{
4
    // Disable to prevent multiple taps while processing

5
    UIButton *captureButton = (UIButton *)sender;
6
    captureButton.enabled = NO;
7
    
8
    // Snap Image from GPU camera, send back to main view controller

9
    [stillCamera capturePhotoAsJPEGProcessedUpToFilter:filter withCompletionHandler:^(NSData *processedJPEG, NSError *error)
10
     {
11
         if([delegate respondsToSelector:@selector(didSelectStillImage:withError:)])
12
         {
13
             [self.delegate didSelectStillImage:processedJPEG withError:error];
14
         }
15
         else
16
         {
17
             NSLog(@"Delegate did not respond to message");
18
         }
19
20
         runOnMainQueueWithoutDeadlocking(^{
21
             [self.navigationController popToRootViewControllerAnimated:YES];
22
         });
23
     }];
24
}

Lines 3-5 above disable the "Take Photo" button to prevent multiple presses while processing.

Lines 7 - 22 use the GPUImage method capturePhotoAsJPEGProcessedUpToFilter:withCompletionHandler: to actually save a JPEG image, check to see if a delegate has been set, and then send the image data on to the delegate if it is set.

Line 19 pops the current view controller, but does so on the main application thread.


Wrap Up

Congratulations! If you've followed the tutorial this far, then you should have a fully functional, advanced photo taking application! If you have questions or feedback, feel free to leave it in the comments section below or send them to me directly over Twitter (@markhammonds).

Thanks for reading!

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.