Chapter    10

Image Recipes

Often times a developer is faced with an all-too common problem: Too much information to display with not enough space to show it. For this, you turn to images. Pictures and graphics allow you to convey a variety of information far beyond simple text, combining emotion, information, and style. In iOS, you have several different methods with which to create, utilize, manipulate, and display images. A fairly new feature applies filters to images, allowing for drastic alteration of display with very little code. By understanding these inherent functionalities and techniques in iOS, you can more easily implement stronger, more powerful, and more informative applications.

Recipe 10-1: Drawing Simple Shapes

From the youngest age, every person is taught the most basic of images, dealing with shapes, colors, and pictures. In iOS too, you can start off with the basics of drawing simple shapes in a view. Many concepts dealt with in these first implementations will end up returning in more complex image-based recipes.

Start by creating a new single-view application project.

Before building the user interface, you will create a custom view that will implement some simple drawing code. So, create a new subclass of UIView called MyView and add the following code to its drawRect: method:

//
//  MyView.m
//  Recipe 10-1: Drawing Simple Shapes
//

#import "MyView.h"

@implementation MyView

// ...

// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    // Draw rectangle
    CGRect drawingRect = CGRectMake(0.0, 20.0f, 100.0f, 180.0f);
    const CGFloat *rectColorComponents =
        CGColorGetComponents([[UIColor greenColor] CGColor]);
    CGContextSetFillColor(context, rectColorComponents);
    CGContextFillRect(context, drawingRect);
    // Draw ellipse
    CGRect ellipseRect = CGRectMake(140.0f, 200.0f, 75.0f, 50.0f);
    const CGFloat *ellipseColorComponents =
        CGColorGetComponents([[UIColor blueColor] CGColor]);
    CGContextSetFillColor(context, ellipseColorComponents);
    CGContextFillEllipseInRect(context, ellipseRect);
}

@end

This method uses the following steps to draw basic shapes:

  1. Obtain a reference to the current “context,” represented by a CGContextRef.
  2. Define a CGRect in which to draw.
  3. Acquire color components for the desired color to fill each shape with.
  4. Set the Fill Color.
  5. Fill the specified shape using the CGContextFillRect() and CGContextFillEllipseInRect() functions.

To actually display this in your preconfigured view, you must add an instance of this class to your user interface. This is done programmatically or through Interface Builder, the latter of which we demonstrate.

In the view controller’s .xib file, drag out a UIView from the Object library in the Utilities pane into your view. Place it with the default spacing on each edge, so that the view looks like Figure 10-1.

9781430245995_Fig10-01.jpg

Figure 10-1.  Building your .xib file with a UIView

While your UIView is selected, go to the Identity inspector in the right panel. Under the Custom Class section, change the Class field from “UIView” to “MyView”, as shown in Figure 10-2. This connects the view you added in Interface Builder with the custom class you created earlier.

9781430245995_Fig10-02.jpg

Figure 10-2.  Connecting a custom class to a UIView

When running this application, you see the output of your drawing commands converted into a visual display, resulting in the simulated view in Figure 10-3.

9781430245995_Fig10-03.jpg

Figure 10-3.  A custom view drawing a green rectangle and a blue ellipse

Thankfully, you are not limited to drawing only rectangles and ellipses. There are a few other functions. You can draw custom shapes by creating “paths” of points connected by lines or curves. As an example, add the following code to the drawRect: method to draw a gray semitransparent parallelogram:


- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    // Draw rectangle
    CGRect drawingRect = CGRectMake(0.0, 20.0f, 100.0f, 180.0f);
    const CGFloat *rectColorComponents =
        CGColorGetComponents([[UIColor greenColor] CGColor]);
    CGContextSetFillColor(context, rectColorComponents);
    CGContextFillRect(context, drawingRect);

    // Draw ellipse
    CGRect ellipseRect = CGRectMake(140.0f, 200.0f, 75.0f, 50.0f);
    const CGFloat *ellipseColorComponents =
        CGColorGetComponents([[UIColor blueColor] CGColor]);
    CGContextSetFillColor(context, ellipseColorComponents);
    CGContextFillEllipseInRect(context, ellipseRect);

    // Draw parallelogram
    CGContextBeginPath(context);
    CGContextMoveToPoint(context, 0.0f, 0.0f);
    CGContextAddLineToPoint(context, 100.0f, 0.0f);
    CGContextAddLineToPoint(context, 140.0f, 100.0f);
    CGContextAddLineToPoint(context, 40.0f, 100.0f);
    CGContextClosePath(context);
    CGContextSetGrayFillColor(context, 0.4f, 0.85f);
    CGContextSetGrayStrokeColor(context, 0.0, 0.0);
    CGContextFillPath(context);
}

Note  When creating these paths, you do not have to add a final line back to your last point. By calling the CGContextClosePath() function, your shape is automatically closed between its ending point and starting point.

When you run your application now, you will see your view with a new parallelogram created from your path, as in Figure 10-4.

9781430245995_Fig10-04.jpg

Figure 10-4.  A custom UIView drawing three different shapes

Recipe 10-2: Programming Screenshots

Just as you can put things into a CGContext, you also can easily take them out. By making use of the UIGraphicsGetImageFromCurrentImageContext() function, you can extract an image from whatever is currently drawn.

You’re going to build on the previous recipe and the app you created there. You add to it a feature that takes a snapshot of the current view whenever the user shakes the device. It displays the snapshot in the lower-right corner of the screen, causing a nice symmetric effect on subsequent shakes.

Start by adding a property to the MyView class to hold the latest snapshot. Open MyView.h and add the following declaration:

//
//  MyView.h
//  Recipe 10-1: Drawing Simple Shapes
//

#import <UIKit/UIKit.h>

@interface MyView : UIView

@property (strong, nonatomic)UIImage *image;

@end

Next, select ViewController.xib to bring up Interface Builder. Open the Assistant editor and create an outlet for the custom view by Ctrl-dragging a blue line from it onto the ViewController.h file. Name the outlet myView. For the outlet declaration to compile you’ll need to import MyView.h:

//
//  ViewController.h
//  Recipe 10-1: Drawing Simple Shapes
//

#import <UIKit/UIKit.h>
#import "MyView.h"

@interface ViewController : UIViewController

@property (weak, nonatomic) IBOutlet MyView *myView;

@end

Now, add the code that will draw the snapshot: to the drawRect: method in MyView.m:

- (void)drawRect:(CGRect)rect
{
    // ...

    if (self.image)
    {
        CGFloat imageWidth = self.frame.size.width / 2;
        CGFloat imageHeight = self.frame.size.height / 2;
        CGRect imageRect = CGRectMake(imageWidth, imageHeight, imageWidth, imageHeight);
        [self.image drawInRect:imageRect];
    }
}

@end

As the next step, add the following methods to the view controller. The code implements shake recognition that triggers the screenshot:

//
//  ViewController.m
//  Recipe 10-1: Drawing Simple Shapes
//

#import "ViewController.h"

@implementation ViewController

// ...

- (BOOL) canBecomeFirstResponder
{
    return YES;
}

- (void) viewWillAppear: (BOOL)animated
{
    [self.view becomeFirstResponder];
    [super viewWillAppear:animated];
}

- (void) viewWillDisappear: (BOOL)animated
{
    [self.view resignFirstResponder];
    [super viewWillDisappear:animated];
}

- (void) motionEnded: (UIEventSubtype)motion withEvent: (UIEvent *)event
{
    if (event.subtype == UIEventSubtypeMotionShake)
    {
        // Device was shaken

        // TODO: Take a screenshot
    }
}

@end

Finally, you implement the code for taking a snapshot and hand it over to the custom view for displaying. But before that, you will need to import the QuartzCore API. Failing to do so causes a compiler error later when you access a layer on the view to draw the screenshot. So, open ViewController.h again and add the following lines of code:

//
//  ViewController.h
//  Recipe 10-1: Drawing Simple Shapes
//

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#import "MyView.h"

@interface ViewController : UIViewController

@property (weak, nonatomic) IBOutlet MyView *myView;

@end

Now you’re ready to finish the implementation of the shake event in ViewController.m:

- (void) motionEnded: (UIEventSubtype)motion withEvent: (UIEvent *)event
{
    if (event.subtype == UIEventSubtypeMotionShake)
    {
        // Device was shaken

        // Acquire image of current layer
        UIGraphicsBeginImageContext(self.view.bounds.size);
        CGContextRef context = UIGraphicsGetCurrentContext();
        [self.view.layer renderInContext:context];
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        self.myView.image = image;
        [self.myView setNeedsDisplay];
    }
}

The setNeedsDisplay method in the UIView class instructs it to re-call its drawRect: method to incorporate any recent changes.

Now, after testing the application again, when shaking the device a couple of times, you should see a screen similar to Figure 10-5.

9781430245995_Fig10-05.jpg

Figure 10-5.  An application showing a screenshot of a screen that was displaying a screenshot already

Note  If you run the app in iOS Simulator you can simulate a shake by pressing Ctrl + Cmd + Z.

Recipe 10-3: Using Image Views

The easiest way to display an image in your application is to use the UIImageView class. In this recipe you create a simple app that displays an image chosen by the user. Later on you’ll build on top of it to take full advantage of the image processing power of iOS.

To enhance the functionality of your application, you will specifically design it for the iPad, and then make use of the UISplitViewController. Create a new project, and select the Master-Detail Application template. On the next screen, after entering the project name Image Recipes, be sure the application’s device-family is set to iPad, as shown in Figure 10-6.

9781430245995_Fig10-06.jpg

Figure 10-6.  Configuring an iPad project

Upon creating your application, Xcode generates a project with a UISplitViewController set up with master and detail view controllers. If your simulator or device is in portrait mode, you will see only the view of the detail view controller, but if you rotate to landscape, then you will get a nice mix of both views. You do not see both views when working in Interface Builder, but if you simulate the app, the generic view will resemble Figure 10-7.

9781430245995_Fig10-07.jpg

Figure 10-7.  An empty UISplitViewController

Now, you configure the detail view controller to include some content. Select the DetailViewController.xib file and use Interface Builder to create the user interface. Add a label (or reuse the one created by default by the template,) an image view and two buttons and arrange them as in Figure 10-8. Note that we’ve changed the background color of the image view to black (set in the Attributes inspector, Background field).

9781430245995_Fig10-08.jpg

Figure 10-8.  A simulated view of your configured user interface

Select the image view and open the Attributes inspector. In the View section, change the Mode attribute from Scale to Fill to Aspect Fill. This makes the image view scale its content so that it fills the image view’s bounds while preserving the proportions of the image. This usually means that a part of the image is drawn outside the frame of the image view. To prevent this you should also check the Drawing option Clip Subviews. Figure 10-9 shows these settings.

9781430245995_Fig10-09.jpg

Figure 10-9.  Configuring an image view to fill with maintained proportions and clip to its bounds

Next, create outlets for the label and the image view, use the names detailDesctriptionLabel and imageView, respectively. Also, create the actions selectImage: and clearImage: for the respective buttons.

Configure your application to display a UIPopoverController containing a UIImagePickerController to allow users to select an image from their phone. To do this, you need your detail view controller to conform to several extra protocols: UIImagePickerControllerDelegate, UINavigationControllerDelegate and UIPopoverControllerDelegate. You also need two properties, one for storing the selected image, and one to reference the UIPopoverController. To incorporate these changes, add the following to the DetailViewController.h file:

//
//  DetailViewController.h
//  Image Recipes
//

#import <UIKit/UIKit.h>

@interface DetailViewController : UIViewController <UISplitViewControllerDelegate ,
    UIImagePickerControllerDelegate,UINavigationControllerDelegate,
    UIPopoverControllerDelegate>

@property (strong, nonatomic) id detailItem;

@property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@property (strong, nonatomic) UIPopoverController *pop;

- (IBAction)selectImage:(id)sender;
- (IBAction)clearImage:(id)sender;

@end

Now you can implement the selectImage: method to present an interface to select an image to display.

-(void)selectImage:(UIButton *)sender
{
    UIImagePickerController *picker = [[UIImagePickerController alloc] init];
    if ([UIImagePickerController
            isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary])
    {
        picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
        picker.delegate = self;

        self.pop = [[UIPopoverController alloc] initWithContentViewController:picker];
        self.pop.delegate = self;
        [self.pop presentPopoverFromRect:sender.frame inView:self.view
            permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
    }
}

You can then implement your UIImagePickerController delegate methods to properly handle the selection of an image or cancellation.

-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
    [self.pop dismissPopoverAnimated:YES];
}

-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    UIImage *image = [info valueForKey:@"UIImagePickerControllerOriginalImage"];
    self.imageView.image = image;

    [self.pop dismissPopoverAnimated:YES];
}

As you can see, you configure the image view to display the selected image by using the image property. You also set the contentMode property to UIViewContentModeScaleAspectFill to ensure that the bounds of your UIImageView are always filled by at least most of the image. To prevent the image view from drawing outside its bounds, you also set the clipsToBounds property to YES.

Finally, you can implement the clearImage: action method to allow your view to be reset:

- (IBAction)clearImage:(id)sender
{
    self.imageView.image = nil;
}

At this point, you can run your application, select an image, and display it in a UIImageView, as shown in Figure 10-10.

9781430245995_Fig10-10.jpg

Figure 10-10.  Your application displaying an image in a UIImageView

Tip  If you are testing the application on the iOS simulator, you will need to have some images to display. The easiest way to save images to the simulator’s photos library is to drag and drop them onto the simulator window. This brings up Safari, where you can click and hold the mouse on the image. You then are given an option to save the image, and after this you can use it in your application.

Recipe 10-4: Scaling Images

Often the images that your applications deal with come from a variety of sources, and usually do not fit your specific view’s display perfectly. To adjust for this, you can implement methods to scale and resize your images.

With an image view, scaling and resizing is easy. For example, in the previous recipe you used Aspect Fill in combination with Clip subviews to scale proportionally and still fill the entire image view. This results in a clipped but nice looking image. Another option is to use the Aspect Fit mode, which also scales the image with retained aspect but makes sure the entire image is displayed. This, of course, may cause some parts of the image view not being filled by the image. If you don’t care about image aspect you can use the default mode Scale to Fill. You can use these and other options by simply changing the Mode attribute of the image view.

However, sometimes you’d like to scale the actual image programmatically, for example if you want to save the resulting image or just optimize the display by providing an already scaled image. In this recipe we’ll show you how to scale an image using code. You’re going to implement two different methods corresponding to the Scale to Fill and the Aspect Fit modes of an image view.

You build on the previous recipe but now use the master view’s table view that contains the three functions: Select Image, Resize Image, and Scale Image. Because these functions operate directly on a UIImage, you need to turn off the inherent scaling function of the image view. Do that by changing its Mode attribute to Center instead of Aspect Fill.

Now, start by creating a method to configure the user interface of your detail view controller. For this you’ll need to add a couple of outlets more to reference the two buttons. Go ahead and create the outlets selectImageButton and clearImageButton, respectively.

Now add the following method declaration to your DetailViewController.h file:

//
//  DetailViewController.h
//  Image Recipes
//

#import <UIKit/UIKit.h>

@interface DetailViewController : UIViewController <UISplitViewControllerDelegate,
                                                    UIImagePickerControllerDelegate,
                                                    UINavigationControllerDelegate,
                                                    UIPopoverControllerDelegate>

@property (strong, nonatomic) id detailItem;

@property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (weak, nonatomic) IBOutlet UIButton *selectImageButton;
@property (weak, nonatomic) IBOutlet UIButton *clearImageButton;

@property (strong, nonatomic) UIPopoverController *pop;

- (IBAction)selectImage:(id)sender;
- (IBAction)clearImage:(id)sender;

- (void)configureDetailsWithImage:(UIImage *)image label:(NSString *)label showsButtons:(BOOL)showButton;

@end

You implement this method like so:

-(void)configureDetailsWithImage:(UIImage *)image label:(NSString *)label showsButtons:(BOOL)showsButton
{
    self.imageView.image = image;
    self.detailDescriptionLabel.text = label;
    if (showsButton == NO)
    {
        self.selectImageButton.hidden = YES;
        self.clearImageButton.hidden = YES;
    }
    else if (showsButton == YES)
    {
        self.selectImageButton.hidden = NO;
        self.clearImageButton.hidden = NO;
    }
}

Next, add a reference to the master view controller in your detail view controller. This allows the image selected to be passed back to the master view controller. Add the following code to DetailViewController.h:

//
//  DetailViewController.h
//  Image Recipes
//

#import <UIKit/UIKit.h>
#import "MasterViewController.h"

@interface DetailViewController : UIViewController <UISplitViewControllerDelegate,
                                                    UIImagePickerControllerDelegate,
                                                    UINavigationControllerDelegate,
                                                    UIPopoverControllerDelegate>

// ...

@property (strong, nonatomic) MasterViewController *masterViewController;

@end

Now, add a property to your master view controller class to store the chosen image:

//
//  MasterViewController.h
//  Image Recipes
//

#import <UIKit/UIKit.h>

@class DetailViewController;

@interface MasterViewController : UITableViewController

@property (strong, nonatomic) DetailViewController *detailViewController;
@property (strong, nonatomic) UIImage *mainImage;

@end

Back in your detail view controller, you update the imagePickerController:didFinishPickingMediaWithInfo: delegate method to update the image of the master view controller:

-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    UIImage *image = [info valueForKey:@"UIImagePickerControllerOriginalImage"];
    self.masterViewController.mainImage = image;
    self.imageView.image = image;
    [self.pop dismissPopoverAnimated:YES];
}

You also adjust the implementation of the clearImage: action method accordingly:

- (IBAction)clearImage:(id)sender
{
    self.imageView.image = nil;
    self.masterViewController.mainImage = nil;
}

In your master view controller, you will later add code to use your images in the actual table, so implement a custom setter method for the mainImage property to reload the UITableView’s data:

-(void)setMainImage:(UIImage *)image
{
    _mainImage = image;
    NSIndexPath *currentIndexPath = self.tableView.indexPathForSelectedRow;
    [self.tableView reloadData];
    [self.tableView selectRowAtIndexPath:currentIndexPath animated:YES
        scrollPosition:UITableViewScrollPositionTop];
}

Next, create two different methods to resize an image. Add the following two class method declarations to your detail view controller’s header file:

//
//  MasterViewController.h
//  Image Recipes
//

#import <UIKit/UIKit.h>

@class DetailViewController;

@interface MasterViewController : UITableViewController

@property (strong, nonatomic) DetailViewController *detailViewController;
@property (strong, nonatomic) UIImage *mainImage;

+ (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)size;
+ (UIImage *)aspectScaleImage:(UIImage *)image toSize:(CGSize)size;

@end

The first method simply re-creates the image within a specified size, ignoring the aspect ratio of the image. Here is the implementation:

+ (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)size
{
    UIGraphicsBeginImageContext(size);
    [image drawInRect:CGRectMake(0, 0, size.width, size.height)];
    UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return scaledImage;
}

The second method, with a little calculation, determines the best way to resize the image to both preserve the aspect ratio and fit inside the given size:

+ (UIImage *)aspectScaleImage:(UIImage *)image toSize:(CGSize)size
{
    if (image.size.height < image.size.width)
    {
        float ratio = size.height / image.size.height;
        CGSize newSize = CGSizeMake(image.size.width * ratio, size.height);

        UIGraphicsBeginImageContext(newSize);
        [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
    }
    else
    {
        float ratio = size.width / image.size.width;
        CGSize newSize = CGSizeMake(size.width, image.size.height * ratio);

        UIGraphicsBeginImageContext(newSize);
        [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)];
    }
    UIImage *aspectScaledImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return aspectScaledImage;
}

To make sure your view controllers are properly interacting, connect the two on application launch. Go to the application:didFinishLaunchingWithOptions: method in the AppDelegate.m file and add the following line:

//
//  AppDelegate.m
//  Image Recipes
//

#import "AppDelegate.h"
#import "MasterViewController.h"
#import "DetailViewController.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.

    MasterViewController *masterViewController = [[MasterViewController alloc] initWithNibName:@"MasterViewController" bundle:nil];
    UINavigationController *masterNavigationController = [[UINavigationController alloc] initWithRootViewController:masterViewController];

    DetailViewController *detailViewController = [[DetailViewController alloc] initWithNibName:@"DetailViewController" bundle:nil];
    UINavigationController *detailNavigationController = [[UINavigationController alloc] initWithRootViewController:detailViewController];

    masterViewController.detailViewController = detailViewController;
    detailViewController.masterViewController = masterViewController;

    self.splitViewController = [[UISplitViewController alloc] init];
    self.splitViewController.delegate = detailViewController;
    self.splitViewController.viewControllers = @[masterNavigationController, detailNavigationController];
    self.window.rootViewController = self.splitViewController;
    [self.window makeKeyAndVisible];
    return YES;
}

// ...

@end

Now, to finish configuring the behavior of the master view controller, modify the following delegate methods:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (self.mainImage == nil)
        return 1;
    else
        return 3;
}

// ...

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }

    if (indexPath.row == 0)
        cell.textLabel.text = NSLocalizedString(@"Selected Image", @"Detail");
    else if (indexPath.row == 1)
        cell.textLabel.text = NSLocalizedString(@"Resized Image", @"Detail");
    else if (indexPath.row == 2)
        cell.textLabel.text = NSLocalizedString(@"Scaled Image", @"Detail");
    return cell;
}

// ...

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (self.mainImage != nil)
    {
        UIImage *image;
        NSString *label;
        BOOL showsButtons = NO;
        if (indexPath.row == 0)
        {
            image = self.mainImage;
            label = @"Select an Image to Display";
            showsButtons = YES;
        }
        else if (indexPath.row == 1)
        {
            image = [MasterViewController scaleImage:self.mainImage
                toSize:self.detailViewController.imageView.frame.size];
            label = @"Chosen Image Resized";
        }
        else if (indexPath.row == 2)
        {
            image = [MasterViewController aspectScaleImage:self.mainImage
                toSize:self.detailViewController.imageView.frame.size];
            label = @"Chosen Image Scaled";
        }
        [self.detailViewController configureDetailsWithImage:image label:label
            showsButtons:showsButtons];
    }
}

You’re done and can now build and run the application. This time, when you select an image you’ll see that it doesn’t scale and (assuming the image is larger than the image view) will be clipped like the one in Figure 10-11.

9781430245995_Fig10-11.jpg

Figure 10-11.  An image of a seagull displayed without scaling

Now, if you select the Resized Image cell in the master view you see the same picture, this time run through the scaleImage:toSize: method you created. The image has been scaled, without considering its original proportions, to the same size as the image view. Figure 10-12 shows an example of this.

9781430245995_Fig10-12.jpg

Figure 10-12.  An image scaled without keeping its proportions

The issue with this option, however, is that the picture has become slightly deformed. This may not be quite obvious with this particular image, but when dealing with images of people, the distortion of physical features is quite obvious and unsightly. To solve this, use the Aspect-Scaled image.

When you select the Scaled Image cell you’ll see the effect of your aspectScaleImage:toSize: method. This method has created a UIImage of a size that fits within the image view but with maintained original proportions. This results in an image without any size distortions but instead it doesn’t fill the entire image view, as shown in Figure 10-13.

9781430245995_Fig10-13.jpg

Figure 10-13.  An alternative method of scaling to remove distortion, resulting in empty spaces on the sides

In Review

You have covered two simple methods for resizing a UIImage, each with their own advantages and issues.

  1. Your first method simply resized the image to a given size, regardless of aspect ratio. While this kept your image from obstructing any other elements, it ended up giving you a fair bit of distortion.
  2. By using a little math, you scaled down your image to a size while manually maintaining the aspect ratio. Because this leaves a blank space around the image, you can apply a black background. This is a useful technique to use when displaying large images in an application that has no control over the original image size. It allows for any image to be comfortably fit in a given space, yet it maintains a visually appealing black background no matter the case.

Recipe 10-5: Manipulating Images with Filters

The Core Image framework, a group of classes that was introduced in iOS 5.0, allows you to creatively apply a great variety of “filters” to images. In this recipe you’ll apply two kinds of filters to an image, the Hue filter and the Straightening filter. The former changes the hue of the image, while the latter rotates an image to straighten it out.

Again you build on the project you created in Recipes 10-3 and 10-4, adding functions to apply these filters.

Start by linking the CoreImage.framework library to your project (see Chapter 1 for a description on how to do this), and then import its API in the MasterViewController.h file. You also need a mutable array property to hold the filtered images you will display. Here’s the MasterViewController.h file with these changes marked in bold:

//
//  MasterViewController.h
//  Image Recipes
//

#import <UIKit/UIKit.h>
#import <CoreImage/CoreImage.h>

@class DetailViewController;

@interface MasterViewController : UITableViewController

@property (strong, nonatomic) DetailViewController *detailViewController;
@property (strong, nonatomic) UIImage *mainImage;
@property (strong, nonatomic) NSMutableArray *filteredImages;

+ (UIImage *)scaleImage:(UIImage *)image toSize:(CGSize)size;
+ (UIImage *)aspectScaleImage:(UIImage *)image toSize:(CGSize)size;

@end

Implement lazy initialization of the filteredImages property by adding the following custom getter to MasterViewController.m:

-(NSMutableArray *)filteredImages
{
    if (!_filteredImages)
    {
        _filteredImages = [[NSMutableArray alloc] initWithCapacity:3];
    }
    return _filteredImages;
}

Now, modify your setMainImage: method again to include handling of this array.

-(void)setMainImage:(UIImage *)image
{
    [self.filteredImages removeAllObjects];
    if (image != nil)
    {
        [self populateImageViewWithImage:image];
    }

    _mainImage = image;
    NSIndexPath *currentIndexPath = self.tableView.indexPathForSelectedRow;
    [self.tableView reloadData];
    [self.tableView selectRowAtIndexPath:currentIndexPath animated:YES
        scrollPosition:UITableViewScrollPositionTop];
}

The populateFilteredImagesWithImage: method, which contains most of your Core Image code, is implemented as follows:

-(void)populateImageViewWithImage:(UIImage *)image
{
    CIImage *main = [[CIImage alloc] initWithImage:image];

    CIFilter *hueAdjust = [CIFilter filterWithName:@"CIHueAdjust"];
    [hueAdjust setDefaults];
    [hueAdjust setValue:main forKey:@"inputImage"];
    [hueAdjust setValue:[NSNumber numberWithFloat: 3.14/2.0f]
                 forKey:@"inputAngle"];
    CIImage *outputHueAdjust = [hueAdjust valueForKey:@"outputImage"];
    CIContext *context = [CIContext contextWithOptions:nil];
    CGImageRef cgImage1 = [context createCGImage:outputHueAdjust
        fromRect:outputHueAdjust.extent];
    UIImage *outputImage1 = [UIImage imageWithCGImage:cgImage1];
    CGImageRelease(cgImage1);
    [self.filteredImages addObject:outputImage1];

    CIFilter *strFilter = [CIFilter filterWithName:@"CIStraightenFilter"];
    [strFilter setDefaults];
    [strFilter setValue:main forKey:@"inputImage"];
    [strFilter setValue:[NSNumber numberWithFloat:3.14f] forKey:@"inputAngle"];
    CIImage *outputStr = [strFilter valueForKey:@"outputImage"];
    CGImageRef cgImage2 = [context createCGImage:outputStr fromRect:outputStr.extent];
    UIImage *outputImage2 = [UIImage imageWithCGImage:cgImage2];
    CGImageRelease(cgImage2);
    [self.filteredImages addObject:outputImage2];
}

As you can see from this method, creating a CIImage requires the following steps:

  1. Obtain a CIImage of the intended input image.
  2. Create a filter using a specific name key. The name defines which filter will be applied, as well as its various parameters that can be used.
  3. Reset all parameters of the filter to defaults for good measure.
  4. Set the input image to the filter using the “inputImage” key.
  5. Set any additional values related to the filter to customize output.
  6. Retrieve the output CIImage using the “outputImage” key.
  7. Create a UIImage from the CIImage by use of a CIContext. Because the CIContext returns a CGImage, which memory is not managed by ARC, you also need to release it using CGImageRelease().

Note  There is a large number of filters that can be applied to images, all with their own specific parameters and keys. To find details for a specific filter, use the Apple documentation at http://developer.apple.com/library/ios/#DOCUMENTATION/GraphicsImaging/Reference/CoreImageFilterReference/Reference/reference.html.

The next thing you’ll do is to add the filter functions to the table view. Start by making the following small change to the tableView:numberOfRowsInSection: delegate method:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (self.mainImage == nil)
        return 1;
    else
        return 5;
}

Also update tableView:cellForRowAtIndexPath: to configure the cells for the new rows:


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
            reuseIdentifier:CellIdentifier];
    }

    if (indexPath.row == 0)
        cell.textLabel.text = NSLocalizedString(@"Selected Image", @"Detail");
    else if (indexPath.row == 1)
        cell.textLabel.text = NSLocalizedString(@"Resized Image", @"Detail");
    else if (indexPath.row == 2)
        cell.textLabel.text = NSLocalizedString(@"Scaled Image", @"Detail");
    else if (indexPath.row == 3)
        cell.textLabel.text = NSLocalizedString(@"Hue Adjust", @"Detail");
    else if (indexPath.row == 4)
        cell.textLabel.text = NSLocalizedString(@"Straighten Filter", @"Detail");
    return cell;
}

Also modify the tableView:didSelectRowAtIndexPath: method:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (self.mainImage != nil)
    {
        UIImage *image;
        NSString *label;
        BOOL showsButtons = NO;
        if (indexPath.row == 0)
        {
            image = self.mainImage;
            label = @"Select an Image to Display";
            showsButtons = YES;
        }
        else if (indexPath.row == 1)
        {
            image = [MasterViewController scaleImage:self.mainImage
                toSize:self.detailViewController.imageView.frame.size];
            label = @"Chosen Image Resized";
        }
        else if (indexPath.row == 2)
        {
            image = [MasterViewController aspectScaleImage:self.mainImage
                toSize:self.detailViewController.imageView.frame.size];
            label = @"Chosen Image Scaled";
        }
        else if (indexPath.row == 3)
        {
            image = [self.filteredImages objectAtIndex:0];
            CGSize contentSize = self.detailViewController.imageView.frame.size;
            image = [MasterViewController aspectScaleImage:image toSize:contentSize];
            label = @"Hue Adjustment";
        }
        else if (indexPath.row == 4)
        {
            image = [self.filteredImages objectAtIndex:1];
            CGSize contentSize = self.detailViewController.imageView.frame.size;
            image = [MasterViewController aspectScaleImage:image toSize:contentSize];
            label = @"Straightening Filter";
        }
        [self.detailViewController configureDetailsWithImage:image label:label
            showsButtons:showsButtons];
    }
}

As you can see, you’re reusing the aspectScaleImage:toSize: method you created in the previous recipe to scale the filtered images so that they will fit nicely within the image view.

When running your application now, you can see the outputs of the two types of filters. Shown in Figure 10-14 is an example of the straighten filter. As you may remember from the code, it specified an angle of pi (3.14) which means a 180 degree rotation and an upside down image.

9781430245995_Fig10-14.jpg

Figure 10-14.  The straightening filter has rotated an image 180 degrees

Combining Filters

It’s easy to apply multiple filters to an image. You just combine them in a series by specifying the output image of one filter as the input image of another. As an example, you’ll add a function that applies both the hue filter and the straightening filter to the selected image.

Add the following code to the populateImagesWithImage: method to create a combination filter:

-(void)populateImageViewWithImage:(UIImage *)image
{
    // ...

    CIFilter *seriesFilter = [CIFilter filterWithName:@"CIStraightenFilter"];
    [seriesFilter setDefaults];
    [seriesFilter setValue:outputHueAdjust forKey:@"inputImage"];
    [seriesFilter setValue:[NSNumber numberWithFloat:3.14/2.0f] forKey:@"inputAngle"];
    CIImage *outputSeries = [seriesFilter valueForKey:@"outputImage"];
    CGImageRef cgImage3 = [context createCGImage:outputSeries
        fromRect:outputSeries.extent];
    UIImage *outputImage3 = [UIImage imageWithCGImage:cgImage3];
    [self.filteredImages addObject:outputImage3];
}

Update the tableView:numberOfRowsInSection: method to show a sixth cell:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (self.mainImage == nil)
        return 1;
    else
        return 6;
}

Likewise, add a sixth case to your tableView:cellForRowAtIndexPath: method to display the name of this fourth cell.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // ...

    if (indexPath.row == 0)
        cell.textLabel.text = NSLocalizedString(@"Selected Image", @"Detail");
    else if (indexPath.row == 1)
        cell.textLabel.text = NSLocalizedString(@"Resized Image", @"Detail");
    else if (indexPath.row == 2)
        cell.textLabel.text = NSLocalizedString(@"Scaled Image", @"Detail");
    else if (indexPath.row == 3)
        cell.textLabel.text = NSLocalizedString(@"Hue Adjust", @"Detail");
    else if (indexPath.row == 4)
        cell.textLabel.text = NSLocalizedString(@"Straighten Filter", @"Detail");
    else if (indexPath.row == 5)
        cell.textLabel.text = NSLocalizedString(@"Series Filter", @"Detail");
    return cell;
}

Finally, add another case to the tableView:didSelectRowAtIndexPath: to initialize the detail view controller with the combined filter image.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (self.mainImage != nil)
    {
        UIImage *image;
        NSString *label;
        BOOL showsButtons = NO;
        if (indexPath.row == 0)
        {
            image = self.mainImage;
            label = @"Select an Image to Display";
            showsButtons = YES;
        }

// ...

        else if (indexPath.row == 5)
        {
            image = [self.filteredImages objectAtIndex:2];
            CGSize contentSize = self.detailViewController.imageView.frame.size;
            image = [MasterViewController aspectScaleImage:image toSize:contentSize];
            label = @"Series Filter";
        }
        [self.detailViewController configureDetailsWithImage:image label:label showsButtons:showsButtons];
    }
}

Now, on testing the application, your new double-filter combines the effects of your previous two, resulting in a hue-adjusted and rotated image; this time with a 90 degree rotation, as shown in Figure 10-15.

9781430245995_Fig10-15.jpg

Figure 10-15.  An image with both a hue filter and a 90-degree straightening filter applied

Note  The majority of the processing work, when dealing with the Core Image framework, comes from when the UIImage is created from the CIImage using the CIContext. The creation of a CIImage itself is a very fast operation. In this application, we have chosen to create all the filtered images at once to allow for quick navigation between each display. This is why, on selecting an image, your simulator may take a couple seconds to actually display the image and refresh. If you were building this application for release, you would want to convey in some way to the user that work is being done through a UIActivityIndicatorView or UIProgressView.

Creating Thumbnail Images for the Table View

As a final touch we’re going to return to the resizing topic of the previous recipe. You’ll implement another aspect-scaling method that does what the Aspect Fill mode of an image view does, that is, scales with maintained proportions but ensures that the entire area is being covered. This method is more suitable for the creation of thumbnail images, which you’re now going to implement for the filter functions in the table view.

Start by adding the new scaling method to the master view controller:

+ (UIImage *)aspectFillImage:(UIImage *)image toSize:(CGSize)size
{
    UIGraphicsBeginImageContext(size);
    if (image.size.height< image.size.width)
    {
        float ratio = size.height/image.size.height;
        [image drawInRect:CGRectMake(0, 0, image.size.width*ratio, size.height)];
    }
    else
    {
        float ratio = size.width/image.size.width;
        [image drawInRect:CGRectMake(0, 0, size.width, image.size.height*ratio)];
    }
    UIImage *aspectScaledImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return aspectScaledImage;
}

Now you just need to modify the tableView:cellForRowAtIndexPath: again to include the selection of an image for the cell’s imageView:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
            reuseIdentifier:CellIdentifier];
    }

    if (indexPath.row == 0)
        cell.textLabel.text = NSLocalizedString(@"Selected Image", @"Detail");
    else if (indexPath.row == 1)
        cell.textLabel.text = NSLocalizedString(@"Resized Image", @"Detail");
    else if (indexPath.row == 2)
        cell.textLabel.text = NSLocalizedString(@"Scaled Image", @"Detail");
    else if (indexPath.row == 3)
    {
        CGSize thumbnailSize = CGSizeMake(120, 75);
        UIImage *displayImage = [self.filteredImages objectAtIndex:0];
        UIImage *thumbnailImage = [MasterViewController aspectFillImage:displayImage
            toSize:thumbnailSize];
        cell.imageView.image = thumbnailImage;
        cell.textLabel.text = NSLocalizedString(@"Hue Adjust", @"Detail");
    }
    else if (indexPath.row == 4)
    {
        CGSize thumbnailSize = CGSizeMake(120, 75);
        UIImage *displayImage = [self.filteredImages objectAtIndex:1];
        UIImage *thumbnailImage = [MasterViewController aspectFillImage:displayImage
            toSize:thumbnailSize];
        cell.imageView.image = thumbnailImage;
        cell.textLabel.text = NSLocalizedString(@"Straighten Filter", @"Detail");
    }
    else if (indexPath.row == 5)
    {
        CGSize thumbnailSize = CGSizeMake(120, 75);
        UIImage *displayImage = [self.filteredImages objectAtIndex:2];
        UIImage *thumbnailImage = [MasterViewController aspectFillImage:displayImage
            toSize:thumbnailSize];
        cell.imageView.image = thumbnailImage;
        cell.textLabel.text = NSLocalizedString(@"Series Filter", @"Detail");
    }
    return cell;
}

When you test your application now, the cells for the hue, straightening, and series filters have a scaled thumbnail version of the larger image they refer to. Figure 10-16 shows an example of this.

9781430245995_Fig10-16.jpg

Figure 10-16.  An application with thumbnails in its table view

Recipe 10-6: Detecting Features

Along with the flexible use of filters, the Core Image framework has also brought the possibility of feature detection. With it, you can “search” images for key components such as faces.

In this recipe you implement a facial detection application. Create a new single-view project for the iPhone device family. Once your project is created, add the Core Image framework to your project, just as in the previous recipe.

Set the background color of the main view to black and add two image views and a button, so that the user interface resembles Figure 10-17.

9781430245995_Fig10-17.jpg

Figure 10-17.  A simple user interface for face recognition

Create outlets for each of the three elements, use the property names mainImageView, findFaceButton, and faceImageView, respectively. Also, create an action with the name findFace for the button.

Next, find an image to be displayed in your application and add it to your project. You can do this by dragging the file from the Finder onto Project Navigator. To properly test this application, try to find an image with an easily visible face.

Now, you can build your viewDidLoad method to configure the image views, as well as set the initial image to your main image view. Be sure to change the name of the image (testimage.jpg in the following code) to your own filename.

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.mainImageView.contentMode = UIViewContentModeScaleAspectFit;
    self.faceImageView.contentMode = UIViewContentModeScaleAspectFit;

    UIImage *image = [UIImage imageNamed:@"testimage.jpg"];
    if (image != nil)
    {
        self.mainImageView.image = image;
    }
    else
    {
        [self.findFaceButton setTitle:@"No Image" forState:UIControlStateNormal];
        self.findFaceButton.enabled = NO;
        self.findFaceButton.alpha = 0.6;
    }
}

Now you can implement the findFace: action method to do the feature detection. You can use this method to determine the location of any faces in the given image, create a UIImage from the last face found, and then display it in the face image view.

- (IBAction)findFace:(id)sender
{
    UIImage *image = self.mainImageView.image;
    CIImage *coreImage = [[CIImage alloc] initWithImage:image];
    CIContext *context = [CIContext contextWithOptions:nil];
    CIDetector *detector =
        [CIDetector detectorOfType:@"CIDetectorTypeFace"context:context
            options:[NSDictionary dictionaryWithObjectsAndKeys:
                @"CIDetectorAccuracyHigh", @"CIDetectorAccuracy", nil]];
    NSArray *features = [detector featuresInImage:coreImage];

    if ([features count] >0)
    {
        CIImage *faceImage =
            [coreImage imageByCroppingToRect:[[features lastObject] bounds]];
        UIImage *face = [UIImage imageWithCGImage:[context createCGImage:faceImage
            fromRect:faceImage.extent]];
        self.faceImageView.image = face;

        [self.findFaceButton setTitle:[NSString stringWithFormat:@"%i Face(s) Found",
            [features count]] forState:UIControlStateNormal];
        self.findFaceButton.enabled = NO;
        self.findFaceButton.alpha = 0.6;
    }
    else
    {
        [self.findFaceButton setTitle:@"No Faces Found"forState:UIControlStateNormal];
        self.findFaceButton.enabled = NO;
        self.findFaceButton.alpha = 0.6;
    }
}

This method contains the following steps:

  1. Acquire a CIImage object from your initial UIImage.
  2. Create a CIContext with which to analyze images.
  3. Create an instance of CIDetector with type and options parameters.

    The type parameter specifies the specific feature to identify. Currently, the only possible value for this is CIDetectorTypeFace, which allows you to specifically look for faces.

    The options parameter allows you to specify the accuracy with which you want to look for features. Low accuracy will be faster, but high accuracy will be more precise.

  4. Create an array of all the features found in your image. Because you specified the CIDetectorTypeFace type, these objects will all be instances of the CIFaceFeature class.
  5. Create a CIImage using the imageByCroppingToRect: method with the original image, as well as the bounds specified by the last CIFaceFeature found in the image. These bounds specify the CGRect in which the face exists.
  6. Create a UIImage out of your CIImage (done exactly as in the previous recipe), and then display it in your UIImageView.

When running your application, you can detect any faces inside your image, which will be displayed in your lower UIImageView, as in Figure 10-18.

9781430245995_Fig10-18.jpg

Figure 10-18.  An application detecting and cropping a face from an image

Summary

Images create our world. From the simplest of picture books that children love to read to the massive amounts of visual data transmitted around the Internet, pictures and images have certainly become one of the key foundations of modern culture. iOS offers great tools to create, handle, manipulate, and display images in your applications. With these simple APIs you can create more interesting and useful apps in less time.

In this chapter you have seen how to draw simple shapes; create screenshot images, using image views; resize images with maintained proportions; use filters to manipulate images and detect faces in a photo. We hope this has given you the head start you need to take full advantage of the powerful graphics features of iOS.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset