Camera - iOS Programming: The Big Nerd Ranch Guide (2014)

iOS Programming: The Big Nerd Ranch Guide (2014)

11. Camera

In this chapter, you are going to add photos to the Homepwner application. You will present a UIImagePickerController so that the user can take and save a picture of each item. The image will then be associated with a BNRItem instance and viewable in the item’s detail view.

Figure 11.1 Homepwner with camera addition

Homepwner with camera addition

Images tend to be very large, so it is a good idea to store images separately from other data. Thus, in this chapter, you are going to create a second store for images. BNRImageStore will fetch and cache images as they are needed. It will also be able to flush the cache when memory runs low.

Displaying Images and UIImageView

Your first step is to have the BNRDetailViewController get and display an image. An easy way to display an image is to put an instance of UIImageView on the screen.

Open Homepwner.xcodeproj and BNRDetailViewController.xib. Then drag an instance of UIImageView onto the view and position it below the label. Resize the image view to be almost as wide as the screen but leave some space at the bottom for an eventual toolbar (Figure 11.2).

Figure 11.2 UIImageView on BNRDetailViewController’s view

UIImageView on BNRDetailViewController’s view

A UIImageView displays an image according to its contentMode property. This property determines where to position and how to resize the content within the image view’s frame. UIImageView’s default value for contentMode is UIViewContentModeScaleToFill, which will adjust the image to exactly match the bounds of the image view. If you keep the default, an image taken by the camera will be contorted to fit into the square UIImageView. You have to change the contentMode of the image view so that it resizes the image with the same aspect ratio.

Select the UIImageView and open the attributes inspector. Find the Mode attribute and change it to Aspect Fit (Figure 11.3). This will resize the image to fit within the bounds of the UIImageView.

Figure 11.3 Change UIImageView’s mode to Aspect Fit

Change UIImageView’s mode to Aspect Fit

Next, Option-click BNRDetailViewController.m in the project navigator to open it in the assistant editor. Control-drag from the UIImageView to the class extension in BNRDetailViewController.m. Name the outlet imageView and choose Weak as the storage type. Click Connect.

BNRDetailViewController’s class extension should now look like this:

@interface BNRDetailViewController ()

@property (weak, nonatomic) IBOutlet UITextField *nameField;

@property (weak, nonatomic) IBOutlet UITextField *serialNumberField;

@property (weak, nonatomic) IBOutlet UITextField *valueField;

@property (weak, nonatomic) IBOutlet UILabel *dateLabel;

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

@end

Adding a camera button

Now you need a button to initiate the photo-taking process. It would be nice to put this button on the navigation bar, but you will need the navigation bar for another button later. Instead, you will create an instance of UIToolbar and place it at the bottom of BNRDetailViewController’s view.

In BNRDetailViewController.xib, drag a UIToolbar from the object library onto the bottom of the view.

A UIToolbar works a lot like a UINavigationBar – you can add instances of UIBarButtonItem to it. However, where a navigation bar has two slots for bar button items, a toolbar has an array of bar button items. You can place as many bar button items in a toolbar as can fit on the screen.

By default, a new instance of UIToolbar that is created in a XIB file comes with one UIBarButtonItem. Select this bar button item and open the attribute inspector. Change the Identifier to Camera, and the item will show a camera icon (Figure 11.4).

Figure 11.4 UIToolbar with bar button item

UIToolbar with bar button item

The camera button needs a target and an action. In previous exercises, you connected an action method in two steps: declaring it in code and then making the connection in the XIB file. Just like with outlets, there is a way to do both steps at once.

In the project navigator, Option-click BNRDetailViewController.m to open it in the assistant editor.

In BNRDetailViewController.xib, select the camera button by first clicking on the toolbar and then the button itself. Then Control-drag from the selected button to the implementation part of BNRDetailViewController.m (Figure 11.5).

Figure 11.5 Creating and connecting an action method from a XIB

Creating and connecting an action method from a XIB

Let go of the mouse, and a window will appear that allows you to specify the type of connection you are creating. From the Connection pop-up menu, choose Action. Then, name this method takePicture: and click Connect (Figure 11.6).

Figure 11.6 Creating the action

Creating the action

Now the stub of the action method is in BNRDetailViewController.m, and the UIBarButtonItem instance in the XIB is hooked up to send this message to the BNRDetailViewController when tapped. The stub should look like this:

- (IBAction)takePicture:(id)sender

{

}

Xcode is smart enough to know when an action method is connected in the XIB file. In Figure 11.7, notice the little circle within a circle in the gutter area next to takePicture:’s method. When this circle is filled in, this action method is connected in a XIB file; an empty circle means that it still needs connecting.

Figure 11.7 Source file connection status

Source file connection status

In a later chapter, you will need a pointer to the UIToolbar itself. Let’s set that up now. Select the toolbar (not the Camera button on the toolbar). Then, Control-drag into the class extension in BNRDetailViewController.m. Name this outlet toolbar and ensure that its storage is Weak.

The interface for BNRDetailViewController now has a toolbar outlet:

@interface BNRDetailViewController ()

@property (weak, nonatomic) IBOutlet UITextField *nameField;

@property (weak, nonatomic) IBOutlet UITextField *serialNumberField;

@property (weak, nonatomic) IBOutlet UITextField *valueField;

@property (weak, nonatomic) IBOutlet UILabel *dateLabel;

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

@property (weak, nonatomic) IBOutlet UIToolbar *toolbar;

@end

If you made any mistakes while making these connections, you will need to open BNRDetailViewController.xib and disconnect any bad connections. (Look for yellow warning signs in the connections inspector.)

Taking Pictures and UIImagePickerController

In the takePicture: method, you will instantiate a UIImagePickerController and present it on the screen. When creating an instance of UIImagePickerController, you must set its sourceType property and assign it a delegate.

Setting the image picker’s sourceType

The sourceType constant that tells the image picker where to get images. It has three possible values:

UIImagePickerControllerSourceTypeCamera

The user will take a new picture.

UIImagePickerControllerSourceTypePhotoLibrary

The user will be prompted to select an album and then a photo from that album.

UIImagePickerControllerSourceTypeSavedPhotosAlbum

The user picks from the most recently taken photos.

Figure 11.8 Examples of three source types

Examples of three source types

The first source type, UIImagePickerControllerSourceTypeCamera, will not work on a device that does not have a camera. So, before using this type, you have to check for a camera by sending the message isSourceTypeAvailable: to the UIImagePickerController class:

+ (BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType;

Sending this message returns a Boolean value for whether the device supports the passed-in source type.

In BNRDetailViewController.m, find the stub for takePicture:. Add the following code to create the image picker and set its sourceType.

- (IBAction)takePicture:(id)sender

{

UIImagePickerController *imagePicker =

[[UIImagePickerController alloc] init];

// If the device has a camera, take a picture, otherwise,

// just pick from photo library

if ([UIImagePickerController

isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {

imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;

} else {

imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;

}

}

Setting the image picker’s delegate

In addition to a source type, the UIImagePickerController instance needs a delegate. When the user selects an image from the UIImagePickerController’s interface, the delegate is sent the message imagePickerController:didFinishPickingMediaWithInfo:. (If the user taps the cancel button, then the delegate receives the message imagePickerControllerDidCancel:.)

The image picker’s delegate will be the instance of BNRDetailViewController. In BNRDetailViewController.m, declare that BNRDetailViewController conforms to the UINavigationControllerDelegate and the UIImagePickerControllerDelegate protocols.

@interface BNRDetailViewController ()

<UINavigationControllerDelegate, UIImagePickerControllerDelegate>

Why UINavigationControllerDelegate? UIImagePickerController’s delegate property is actually inherited from its superclass, UINavigationController, and while UIImagePickerController has its own delegate protocol, its inherited delegate property is declared to point to an object that conforms toUINavigationControllerDelegate.

In BNRDetailViewController.m, add the following code to takePicture: to set the instance of BNRDetailViewController to be the image picker’s delegate.

- (IBAction)takePicture:(id)sender

{

UIImagePickerController *imagePicker =

[[UIImagePickerController alloc] init];

// If the device has a camera, take a picture, otherwise,

// just pick from photo library

if ([UIImagePickerController

isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {

imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;

} else {

imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;

}

imagePicker.delegate = self;

}

Presenting the image picker modally

Once the UIImagePickerController has a source type and a delegate, it is time to get its view on the screen. Unlike other UIViewController subclasses you have used, an instance of UIImagePickerController is presented modally. A modal view controller takes over the entire screen until it has finished its work.

To present a view controller modally, you send presentViewController:animated:completion: to the UIViewController whose view is on the screen. The view controller to be presented is passed to it, and this view controller’s view slides up from the bottom of the screen. (You will learn more about the details of presenting modal view controllers in Chapter 17.)

In BNRDetailViewController.m, add code to the end of takePicture: to present the UIImagePickerController.

imagePicker.delegate = self;

// Place image picker on the screen

[self presentViewController:imagePicker animated:YES completion:nil];

}

(The third argument, completion:, expects a block. You will learn about completion blocks in Chapter 17.)

You can build and run the application now. Select a BNRItem to see its details and then tap the camera button on the UIToolbar. UIImagePickerController’s interface will appear on the screen (Figure 11.9), and you can take a picture or choose an existing image if your device does not have a camera.

(If you are working on the simulator, you can open Safari in the simulator and navigate to a page with an image. Click and hold the image and then choose Save Image to save it in the simulator’s photo library. Then this image will be shown in the image picker. The the simulator can be flaky, so you might have to try a few different images before one saves to the library.)

Figure 11.9 UIImagePickerController preview interface

UIImagePickerController preview interface

Saving the image

Selecting an image dismisses the UIImagePickerController and returns you to the detail view. However, you do not have a reference to the photo once the image picker is dismissed. To fix this, you are going to implement the delegate methodimagePickerController:didFinishPickingMediaWithInfo:. This message is sent to the image picker’s delegate when a photo has been selected.

In BNRDetailViewController.m, implement this method to put the image into the UIImageView and then send a message to dismiss the image picker.

- (void)imagePickerController:(UIImagePickerController *)picker

didFinishPickingMediaWithInfo:(NSDictionary *)info

{

// Get picked image from info dictionary

UIImage *image = info[UIImagePickerControllerOriginalImage];

// Put that image onto the screen in our image view

self.imageView.image = image;

// Take image picker off the screen -

// you must call this dismiss method

[self dismissViewControllerAnimated:YES completion:nil];

}

Build and run the application again. Take (or select) a photo. The image picker is dismissed, and you are returned to the BNRDetailViewController’s view.

You could have hundreds of items, and each one could have a large image associated with it. Keeping hundreds of instances of BNRItem in memory is not a big deal. Keeping hundreds of images in memory would be bad: First, you will get a low memory warning. Then, if your app’s memory footprint continues to grow, the operating system will terminate it. The solution, which you are going to implement in the next section, is to store images to disk and only fetch them into RAM when they are needed. This fetching will be done by a new class, BNRImageStore. When theBNRImageStore receives a low-memory notification, it will flush its cache to free the memory that the fetched images were occupying.

Creating BNRImageStore

The image store will hold the pictures the user takes. In Chapter 18, you will have instances of BNRItem write out their instance variables to a file, which will then be read in when the application starts. However, because images tend to be very large, it is a good idea to keep them separate from other data. The image store will fetch and cache the images as they are needed. It will also be able to flush the cache if the device runs low on memory.

Create a new NSObject subclass called BNRImageStore. Open BNRImageStore.h and create its interface:

#import <Foundation/Foundation.h>

@interface BNRImageStore : NSObject

+ (instancetype)sharedStore;

- (void)setImage:(UIImage *)image forKey:(NSString *)key;

- (UIImage *)imageForKey:(NSString *)key;

- (void)deleteImageForKey:(NSString *)key;

@end

In BNRImageStore.m, add a class extension to declare a property to hang onto the images.

@interface BNRImageStore ()

@property (nonatomic, strong) NSMutableDictionary *dictionary;

@end

@implementation BNRImageStore

Like the BNRItemStore, the BNRImageStore needs to be a singleton. In BNRImageStore.m, write the following code to ensure BNRImageStore’s singleton status.

@implementation BNRImageStore

+ (instancetype)sharedStore

{

static BNRImageStore *sharedStore = nil;

if (!sharedStore) {

sharedStore = [[self alloc] initPrivate];

}

return sharedStore;

}

// No one should call init

- (instancetype)init

{

@throw [NSException exceptionWithName:@"Singleton"

reason:@"Use +[BNRImageStore sharedStore]"

userInfo:nil];

return nil;

}

// Secret designated initializer

- (instancetype)initPrivate

{

self = [super init];

if (self) {

_dictionary = [[NSMutableDictionary alloc] init];

}

return self;

}

Then, implement the other three methods declared in the header file.

- (void)setImage:(UIImage *)image forKey:(NSString *)key

{

[self.dictionary setObject:image forKey:key];

}

- (UIImage *)imageForKey:(NSString *)key

{

return [self.dictionary objectForKey:key];

}

- (void)deleteImageForKey:(NSString *)key

{

if (!key) {

return;

}

[self.dictionary removeObjectForKey:key];

}

NSDictionary

Notice that the dictionary is an instance of NSMutableDictionary. Like an array, a dictionary is a collection object that has an immutable version (NSDictionary) and a mutable version (NSMutableDictionary).

Dictionaries and arrays differ in how they store their objects. An array is an ordered list of pointers to objects that is accessed by an index. When you have an array, you can ask it for the object at the nth index:

// Put some object at the beginning of an array

[someArray insertObject:someObject atIndex:0];

// Get that same object out

someObject = [someArray objectAtIndex:0];

A dictionary’s objects are not ordered within the collection. So instead of accessing entries with an index, you use a key. The key is usually an instance of NSString.

// Add some object to a dictionary for the key "MyKey"

[someDictionary setObject:someObject forKey:@"MyKey"];

// Get that same object out

someObject = [someDictionary objectForKey:@"MyKey"];

We call each entry in a dictionary a key-value pair. The value is the object being stored in the collection, and the key is a unique value (usually a string) that you use to store and retrieve the value later. (In other development environments, a dictionary is called a hash map, hash table, orassociative array, but we still use the term key-value pair to talk about the information they store.)

Figure 11.10 NSDictionary diagram

NSDictionary diagram

There are a lot of uses for an NSDictionary. The two most common are flexible data structures and lookup tables.

First, let’s talk about flexible data structures. Typically, when you want to represent a model object, you create a subclass of NSObject and give it appropriate instance variables. For example, a Person class would have instance variables like firstName, age, and other things that you expect a real-life person to have. An instance of NSDictionary can also be used to represent a model object. In the person example, it would contain values for the keys firstName, age, and other things that you expect a real-life person to have.

The difference is that the Person class requires you to define exactly what a Person is and you cannot add, remove, or change the structural make-up of a person. With an NSDictionary, if you wanted to add an address to the “Person”, you could simply add a value for the address key.

This is not an endorsement to use NSDictionary to represent every object – most objects need to have a rigid definition, rules for the way they store, save, and load data, and behavior beyond just storing data. That usually means defining a custom class, like BNRItem. However, NSDictionary is commonly used to represent data that is passed into or returned from a method that can have a different structure depending on options you have specified. For example, the UIImagePickerController’s delegate method hands you an NSDictionary that could contain an image or a video depending on how you configured the image picker. The dictionary could also contain metadata related to that image or video.

The other common usage of NSDictionary is creating lookup tables. Sometime early in your programming career, you probably did something like this:

- (void)changeCharacterClass:(id)sender

{

NSString *enteredText = textField.text;

CharacterClass *cc = nil;

if ([enteredText isEqualToString:@"Warrior"]) {

cc = knight;

} else if ([enteredText isEqualToString:@"Mage"]) {

cc = wizard;

} else if ([enteredText isEqualToString:@"Thief"]) {

cc = rogue;

}

character.characterClass = cc;

}

A dictionary can solve the problem of creating giant if-else or switch statements by pre-determining the mapping between two objects. Continuing with perhaps the nerdiest example of all time, an NSDictionary could be initialized like so:

NSMutableDictionary *lookup = [[NSMutableDictionary alloc] init];

[lookup setObject:knight forKey:@"Warrior"];

[lookup setObject:wizard forKey:@"Mage"];

[lookup setObject:rogue forKey:@"Thief"];

and then you can change the changeCharacterClass: method to something much cleaner:

- (void)changeCharacterClass:(id)sender

{

character.characterClass = [lookup objectForKey:textField.text];

}

The added bonus with this approach is that you do not have to hard-code all the possibilities, but could store them in a data file, get them from a server somewhere, or dynamically add them given some input from the user. This is how the BNRImageStore will work: a key will be generated to map to an image and used to lookup that image later.

When using a dictionary, there can only be one object for each key. If you add an object to a dictionary with a key that matches the key of an object already present in the dictionary, the earlier object is removed. If you need to store multiple objects under one key, you can put them in an array and add the array to the dictionary as the value.

Dictionaries, like arrays, can be created using shorthand syntax. The shorthand syntax for dictionary creation uses curly braces (@{}), unlike the square brackets that NSArray uses (@[]). When initializing a dictionary using shorthand syntax, each key-value pair is separated by a comma (,). A colon (:) is placed between the key and its value.

NSDictionary *dictionary = @{@"key": object, @"anotherKey": anotherObject};

Dictionaries also have a shorthand syntax for retrieving objects:

id object = dictionary[@"key"];

// same as

id object = [dictionary objectForKey:@"key"];

If you have an NSMutableDictionary, you can set the object for a key with shorthand syntax:

dictionary[@"key"] = object;

// same as

[dictionary setObject:object forKey:@"key"];

Update the image store to use the shorthand form of accessing and modifying dictionaries.

- (void)setImage:(UIImage *)image forKey:(NSString *)key

{

[self.dictionary setObject:image forKey:key];

self.dictionary[key] = image;

}

- (UIImage *)imageForKey:(NSString *)key

{

return [self.dictionary objectForKey:key];

return self.dictionary[key];

}

Finally, note that a dictionary’s memory management is like that of an array. Whenever you add an object to a dictionary, the dictionary owns it, and whenever you remove an object from a dictionary, the dictionary releases its ownership.

Creating and Using Keys

When an image is added to the store, it will be put into a dictionary under a unique key, and the associated BNRItem object will be given that key. When the BNRDetailViewController wants an image from the store, it will ask its item for the key and search the dictionary for the image. Add a property to BNRItem.h to store the key.

@property (nonatomic, readonly, strong) NSDate *dateCreated;

@property (nonatomic, copy) NSString *itemKey;

The image keys need to be unique in order for your dictionary to work. While there are many ways to hack together a unique string, you are going to use the Cocoa Touch mechanism for creating universally unique identifiers (UUIDs), also known as globally unique identifiers (GUIDs). Objects of type NSUUID represent a UUID and are generated using the time, a counter, and a hardware identifier, which is usually the MAC address of the WiFi card. When represented as a string, UUIDs look something like this:

4A73B5D2-A6F4-4B40-9F82-EA1E34C1DC04

Import BNRImageStore.h at the top of BNRDetailViewController.m.

#import "BNRDetailViewController.h"

#import "BNRItem.h"

#import "BNRImageStore.h"

In BNRItem.m, modify the designated initializer to generate a UUID and set it as the itemKey.

- (instancetype)initWithItemName:(NSString *)name

valueInDollars:(int)value

serialNumber:(NSString *)sNumber

{

// Call the superclass's designated initializer

self = [super init];

// Did the superclass's designated initializer succeed?

if (self) {

// Give the instance variables initial values

_itemName = name;

_serialNumber = sNumber;

_valueInDollars = value;

// set _dateCreated to the current date and time

_dateCreated = [[NSDate alloc] init];

// Create an NSUUID object - and get its string representation

NSUUID *uuid = [[NSUUID alloc] init];

NSString *key = [uuid UUIDString];

_itemKey = key;

}

// Return the address of the newly initialized object

return self;

}

Then, in BNRDetailViewController.m, update imagePickerController:didFinishPickingMediaWithInfo: to store the image in the BNRImageStore.

- (void)imagePickerController:(UIImagePickerController *)picker

didFinishPickingMediaWithInfo:(NSDictionary *)info

{

UIImage *image = info[UIImagePickerControllerOriginalImage];

// Store the image in the BNRImageStore for this key

[[BNRImageStore sharedStore] setImage:image

forKey:self.item.itemKey];

imageView.image = image;

[self dismissViewControllerAnimated:YES completion:nil];

}

Each time an image is captured, it will be added to the store. Both the BNRImageStore and the BNRItem will know the key for the image, so both will be able to access it as needed.

Similarly, when an item is deleted, you need to delete its image from the image store. At the top of BNRItemStore.m, import the header for the BNRImageStore and add the following code to removeItem:.

#import "BNRImageStore.h"

@implementation BNRItemStore

- (void)removeItem:(BNRItem *)item

{

NSString *key = item.itemKey;

[[BNRImageStore sharedStore] deleteImageForKey:key];

[self.privateItems removeObjectIdenticalTo:item];

}

You might be thinking, “Why not give the BNRItem a pointer to the image? After all, isn’t a pointer to the image a more direct way of referring to the image?” While this is correct, you must consider what happens when you begin saving the items and their images to the filesystem inChapter 18.

When a UIImage is first created, it exists in memory at a specific address. A pointer holds onto this address so you can refer to the image again. The next time the application launches, however, the image will not be at the same address in memory, so you cannot use the same pointer to access it. Instead, the key will be used to name the image file on the filesystem and each BNRItem will hang on to its key. When you want to load the image back into memory, the BNRImageStore will use the itemKey of a BNRItem to find the image file on the filesystem, load it into memory, and return a pointer to the new UIImage instance. Therefore, the key is a persistent way of referring to an image.

Wrapping up BNRImageStore

Now that the BNRImageStore can store images and instances of BNRItem have a key to get that image (Figure 11.11), you need to teach BNRDetailViewController how to grab the image for the selected BNRItem and place it in its imageView.

Figure 11.11 Cache

Cache

The BNRDetailViewController’s view will appear at two times: when the user taps a row in BNRItemsViewController and when the UIImagePickerController is dismissed. In both of these situations, the imageView should be populated with the image of the BNRItem being displayed.

In BNRDetailViewController.m, add code to viewWillAppear: to do this.

- (void)viewWillAppear:(BOOL)animated

{

[super viewWillAppear:animated];

self.nameField.text = item.itemName;

self.serialNumberField.text = item.serialNumber;

self.valueField.text = [NSString stringWithFormat:@"%d",

item.valueInDollars];

static NSDateFormatter *dateFormatter = nil;

if (!dateFormatter) {

dateFormatter = [[NSDateFormatter alloc] init];

dateFormatter.dateStyle = NSDateFormatterMediumStyle;

dateFormatter.timeStyle = NSDateFormatterNoStyle;

}

self.dateLabel.text = [dateFormatter stringFromDate:item.dateCreated];

NSString *imageKey = self.item.imageKey;

// Get the image for its image key from the image store

UIImage *imageToDisplay = [[BNRImageStore sharedStore] imageForKey:imageKey];

// Use that image to put on the screen in the imageView

self.imageView.image = imageToDisplay;

}

If there is no image associated with the item, then imageForKey: will return nil. When the image is nil, the UIImageView will not display an image.

Build and run the application. Create a BNRItem and select it from the UITableView. Then, tap the camera button and take a picture. The image will appear as it should.

Dismissing the Keyboard

When the keyboard appears on the screen in the item detail view, it obscures BNRDetailViewController’s imageView. This is annoying when you are trying to see an image, so you are going to implement the delegate method textFieldShouldReturn: to have the text field resign its first responder status to dismiss the keyboard when the return key is tapped. (This is why you hooked up the delegate outlets earlier.) But first, in BNRDetailViewController.m, have BNRDetailViewController conform to the UITextFieldDelegate protocol.

@interface BNRDetailViewController ()

<UINavigationControllerDelegate, UIImagePickerControllerDelegate,

UITextFieldDelegate>

In BNRDetailViewController.m, implement textFieldShouldReturn:.

- (BOOL)textFieldShouldReturn:(UITextField *)textField

{

[textField resignFirstResponder];

return YES;

}

It would be stylish to also dismiss the keyboard if the user taps anywhere else on BNRDetailViewController’s view. You can dismiss the keyboard by sending the view the message endEditing:, which will cause the text field (as a subview of the view) to resign as first responder. Now let’s figure out how to get the view to send a message when tapped.

You have seen how classes like UIButton can send an action message to a target when tapped. Buttons inherit this target-action behavior from their superclass, UIControl. You are going to change the view of BNRDetailViewController from an instance of UIView to an instance of UIControl so that it can handle touch events.

In BNRDetailViewController.xib, select the main View object. Open the identity inspector and change the view’s class to UIControl (Figure 11.12).

Figure 11.12 Changing the class of BNRDetailViewController’s view

Changing the class of BNRDetailViewController’s view

Then, open BNRDetailViewController.m in the assistant editor. Control-drag from the view (now a UIControl) to the implementation of BNRDetailViewController. When the pop-up window appears, select Action from the Connection pop-up menu. Notice that the interface of this pop-up window is slightly different than the one you saw when creating and connecting the UIBarButtonItem. A UIBarButtonItem is a simplified version of UIControl – it only sends its target an action message when it is tapped. A UIControl, on the other hand, can send action messages in response to a variety of events.

Therefore, you must choose the appropriate event type to trigger the action message being sent. In this case, you want the action message to be sent when the user taps on the view. Configure this pop-up window to appear as it does in Figure 11.13 and click Connect.

Figure 11.13 Configuring a UIControl action

Configuring a UIControl action

This will create a stub method in BNRDetailViewController.m. Update that method:

- (IBAction)backgroundTapped:(id)sender

{

[self.view endEditing:YES];

}

Build and run your application and test both ways of dismissing the keyboard.

Bronze Challenge: Editing an Image

UIImagePickerController has a built-in interface for editing an image once it has been selected. Allow the user to edit the image and use the edited image instead of the original image in BNRDetailViewController.

Silver Challenge: Removing an Image

Add a button that clears the image for an item.

Gold Challenge: Camera Overlay

A UIImagePickerController has a cameraOverlayView property. Make it so that presenting the UIImagePickerController shows a crosshair in the middle of the image capture area.

For the More Curious: Navigating Implementation Files

Both of your view controllers have quite a few methods in their implementation files. To be effective iOS developers, you must be able to go to the code you are looking for quickly and easily. The source editor jump bar in Xcode is one tool at your disposal to help out with this (Figure 11.14).

Figure 11.14 Source editor jump bar

Source editor jump bar

The jump bar shows you where exactly you are within the project (and also where the cursor is within a given file). Figure 11.15 breaks down the jump bar details.

Figure 11.15 Jump bar details

Jump bar details

The breadcrumb trail navigation of the jump bar mirrors the project navigation hierarchy. If you click on any of the sections, you will be presented with a popover of that section in the project hierarchy, and from there you can easily navigate to other parts of the project.

Figure 11.16 shows off the file popover in the Homepwner application.

Figure 11.16 File popover

File popover

Perhaps most useful is the ability to navigate easily within an implementation file. If you click on the last element in the breadcrumb trail, you will get a popover with the contents of the file including all of the methods implemented within that file.

While the popover is still visible, you can start typing to filter the items in the list. At any point, you can then use the up and down arrow keys and then press the Enter key to jump to that method in the code. Figure 11.17 shows what you get when you search for “indexpath” inBNRItemsViewController.m.

Figure 11.17 File popover with “indexpath” search

File popover with indexpath search

#pragma mark

As your classes get longer, it can get more difficult to find the method you are looking for when it is buried in a long list of methods. A good way to organize your methods to help with the mess is by using the #pragma mark preprocessor directive.

#pragma mark - View life cycle

- (void)viewDidLoad {...}

- (void)viewWillAppear:(BOOL)animated {...}

#pragma mark - Actions

- (void)addNewItem:(id)sender {...}

Adding #pragma marks to your code does not change anything with the code, but instead helps Xcode understand how you would like to visually organize your methods. You can see the results of adding them by opening the current file item in the jump bar. Figure 11.18 shows the results of a well-organized BNRItemsViewController.m.

Figure 11.18 File popover with #pragma marks

File popover with #pragma marks

Two useful #pragma marks are the divider and the label.

// This is a divider

#pragma mark -

// This is a label

#pragma mark My Awesome Methods

// They can be combined as well

#pragma mark - My Awesome Methods

By using the #pragma mark directive, you force yourself to organize your code. If done well, this will make your code more readable and easier for you to work with when you inevitably need to revisit the code. After doing it repeatedly, you will build habits which will further help you navigate your code base.

For the More Curious: Recording Video

Once you understand how to use UIImagePickerController to take pictures, making the transition to recording video is trivial. Recall that an image picker controller has a sourceType property that determines whether an image comes from the camera, photo library, or saved photos album. Image picker controllers also have a mediaTypes property, which is an array of strings that contains identifiers for what types of media can be selected from the three source types.

There are two types of media a UIImagePickerController can select: still images and video. By default, the mediaTypes array only contains the constant string kUTTypeImage. Thus, if you do not change the mediaTypes property of an image picker controller, the camera will only allow the user to take still photos, and the photo library and saved photos album will only display images.

Adding the ability to record video or choose a video from the disk is as simple as adding the constant string kUTTypeMovie to the mediaTypes array. However, not all devices support video through the UIImagePickerController. Just like the class method isSourceTypeAvailable: allows you to determine if the device has a camera, the availableMediaTypesForSourceType: method checks to see if that camera can capture video. To set up an image picker controller that can record video or take still images, you would write the following code:

UIImagePickerController *ipc = [[UIImagePickerController alloc] init];

NSArray *availableTypes = [UIImagePickerController

availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera];

ipc.mediaTypes = availableTypes;

ipc.sourceType = UIImagePickerControllerSourceTypeCamera;

ipc.delegate = self;

Now when this image picker controller interface is presented to the user, there will be a switch that allows them to choose between the still image camera or the video recorder. If the user chooses to record a video, you need to handle that in the UIImagePickerController delegate methodimagePickerController:didFinishPickingMediaWithInfo:.

When dealing with still images, the info dictionary that is passed as an argument contains the full image as a UIImage object. However, there is no “UIVideo” class. (Loading an entire video into memory at once would be tough to do with iOS device memory constraints.) Therefore, recorded video is written to disk in a temporary directory. When the user finalizes the video recording, imagePickerController:didFinishPickingMediaWithInfo: is sent to the image picker controller’s delegate, and the path of the video on the disk is in the info dictionary. You can get the path like so:

- (void)imagePickerController:(UIImagePickerController *)picker

didFinishPickingMediaWithInfo:(NSDictionary *)info

{

NSURL *mediaURL = info[UIImagePickerControllerMediaURL];

}

You will learn about the filesystem in Chapter 18, but what you should know now is that the temporary directory is not a safe place to store the video. It needs to be moved to another location.

- (void)imagePickerController:(UIImagePickerController *)picker

didFinishPickingMediaWithInfo:(NSDictionary *)info

{

NSURL *mediaURL = info[UIImagePickerControllerMediaURL];

if (mediaURL) {

// Make sure this device supports videos in its photo album

if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum([mediaURL path])) {

// Save the video to the photos album

UISaveVideoAtPathToSavedPhotosAlbum([mediaURL path], nil, nil, nil);

// Remove the video from the temporary directory

[[NSFileManager defaultManager] removeItemAtPath:[mediaURL path]

error:nil];

}

}

}

That is really all there is to it. There is just one situation that requires some additional information: suppose you want to restrict the user to choosing only videos. Restricting the user to images is simple (leave mediaTypes as the default). Allowing the user to choose between images and videos is just as simple (pass the return value from availableMediaTypesForSourceType:). However, to allow video only, you have to jump through a few hoops. First, you must make sure the device supports video, and then you must set the mediaTypes property to an array containing only the identifier for video.

NSArray *availableTypes = [UIImagePickerController

availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera];

if ([availableTypes containsObject:(__bridge NSString *)kUTTypeMovie]) {

[ipc setMediaTypes:@[(__bridge NSString *)kUTTypeMovie]];

}

Wondering why kUTTypeMovie is cast to an NSString? This constant is declared as:

const CFStringRef kUTTypeMovie;

If you build this code, it will fail, and the compiler will complain that it has never heard of kUTTypeMovie. Oddly enough, both kUTTypeMovie and kUTTypeImage are declared and defined in another framework – MobileCoreServices. You have to explicitly add this framework and import its header file into your project to use these two constants.