Chapter 4. iPad Interface Elements

WHAT'S IN THIS CHAPTER?

  • Building a master-detail view using the UISplitViewController

  • Displaying informational messages using the UIPopoverController

  • Recognizing and reacting to various user gestures using the UIGestureRecognizer

  • Sharing files between the iPad and desktop machines with file-sharing support

In the previous chapter, you learned how to display your application data using the UITableView control. Then, you took that knowledge a step further by building custom cells, adding search and filter capabilities, and implementing indexes and section headers.

In this chapter, you learn about the interface elements and other features that are unique to the iPad. You will then apply this knowledge to build better data-driven applications.

You will explore the UISplitViewController and UIPopoverController, which you can use when building iPad applications. You will also learn how to use the new UIGestureRecognizer class and its subclasses to interpret user gestures. Finally, you will learn how to share files between the iPad and desktop computers using file-sharing support.

DISPLAYING MASTER/DETAIL DATA WITH THE UISPLITVIEWCONTROLLER

Quite often, when you're building a data centric application, you will want to show data in a master/detail format. For instance, in an address book application, the master view typically consists of a simple list of contact names. Selecting a contact from the list then presents the details for that contact such as address, phone number, and company.

On the iPhone, you generally build master/detail displays using the Navigation Controller. You would typically use a UITableView to show the master records. Tapping on a master record advances the user to a second screen that displays the details for the record that the user selected. Because there is very limited space on the iPhone screen, two separate screens are necessary to display the master and detail data. The catalog application that you built in the previous chapter uses this model, as shown in Figure 4-1.

Master-Detail view on the iPhone

Figure 4.1. Master-Detail view on the iPhone

Introducing the UISplitViewController

With the extended amount of screen space available to you on the iPad, you have a better option. Instead of displaying the master data and detail data separately on two different screens, you can combine the two so that all of this information is visible at the same time. The release of the iPhone SDK 3.2 included new functionality specifically designed for the iPad. In this release, Apple introduced a new control called the UISplitViewController. You can use this new user interface element to display two distinct View Controllers side by side in landscape mode, as you can see in Figure 4-2. This enables you to take advantage of the extra screen space afforded by the iPad's large screen.

UISplitViewController in Landscape Mode

Figure 4.2. UISplitViewController in Landscape Mode

In portrait mode, the controller allows the user to view the detail data onscreen while popping up the master data on demand by tapping a button in the interface. Figure 4-3 shows the same View Controller as Figure 4-2, but in this case, I have rotated the iPad into portrait mode. The UISplitViewController automatically changes the display of its child View Controllers based on the orientation of the device.

The UISplitViewController does not do anything to support communication between its child View Controllers. That code is your responsibility. In the template code when you start a new Split View–based Application project, the RootViewController maintains a reference to the DetailViewController, which is set in the MainWindow.xib file. However, you are free to delete this implementation and build the communication between the two views in any way that you choose.

UISplitViewController in portrait mode

Figure 4.3. UISplitViewController in portrait mode

The UISplitViewControllerDelegate Protocol

When the orientation of the device changes, the UISplitViewController calls its delegate methods. The UISplitViewControllerDelegate protocol defines these methods.

The Split View Controller calls the splitViewController:popoverController:willPresentViewController: method when the popover containing the Root View Controller should appear. This happens when the user taps the button in the toolbar while the device is in the portrait orientation. If you are using any other popover controls in your application, you should make sure that you dismiss them in this delegate method. It is a violation of the Apple Human Interface Guidelines to display more than one Popover Controller at a time. If you do, Apple will reject your application during the review process.

The Split View Controller calls the splitViewController:willHideViewController:withBarButtonItem:forPopoverController: method when the View Controller passed into the method is hidden. This occurs when the user rotates the device from landscape to portrait orientation. In this method, you need to add the button that will call the Popover Controller to the toolbar. The application template code implements this method for you.

The Split View Controller calls the splitViewController:willShowViewController:invalidatingBarButtonItem: method when the user rotates the device from portrait orientation back to landscape orientation. This indicates that the left View Controller will be displayed again. The code in this method should remove the toolbar button from the toolbar that the interface uses in portrait mode to display the popover. Again, this is implemented already if you start your project by using the Split View–based Application template.

Starting the Split View Sample Application

In this chapter, you build a sample application that implements some very basic survey features. Survey takers that are out in the field collecting data could use an application like this. The application will use a UISplitViewController to display the names of the people that the user has surveyed on the left side as the master data. The right side will display the actual survey data that the user entered as the child data. Figure 4-4 shows the completed application in landscape mode.

Completed survey application

Figure 4.4. Completed survey application

The application will allow the user to add new survey responses to the data set and view existing responses. To keep the application focused on demonstrating the user interface features that are unique to the iPad, you will not be implementing every feature that would be required for a complete application. For instance, I won't cover adding the code to modify existing surveys, or to delete surveys.

Note

You need at least the iPhone SDK 3.2 to develop applications for the iPad.

To begin the Survey application, open Xcode and create a new project. Choose to use the Split View–based Application template for the new project. Make sure that you leave the "Use Core Data for storage" checkbox unchecked. You will learn about Core Data in the next section of the book. Call your new application Survey.

Just to see the basic functionality that you get from the template code, build and run the application. When the iPad simulator starts, you will see the simulated iPad in portrait mode. Notice how the detail view takes up the whole screen. Click on the Root List button and you will see a View Controller with some data in a popover control as you saw in Figure 4-3.

Press Command+Left Arrow to rotate the iPad in the simulator. You will see the split view change to show the detail in the right pane and the root list in the left pane, as shown in Figure 4-2.

Now, you will take a brief look at the code and XIB that the template provides. First, double-click the MainWindow.xib file located in the project's Resources folder. This will open the file in Interface Builder so that you can look at the structure of the interface.

In the Interface Builder window, click the disclosure triangle next to the Split View Controller item. Next, open the Navigation Controller by clicking the disclosure icon next to it. You should now see the Root View Controller. The thing to notice here is that the Split View Controller does not actually do anything on its own. It is simply a container for the Root View Controller, which implements the left-hand pane, and the Detail View Controller, which implements the right-hand pane.

Next, select the Detail View Controller and press Command+2 to bring up the connections pane for the Detail View Controller. Click the disclosure triangle next to Multiple to see the referencing outlets for the detailViewController. You will see that the App Delegate and Root View Controller reference the detailViewController. This reference is the communication mechanism that allows the Root View Controller to set the data that the Detail View Controller will display.

Next, select the Survey App Delegate. If you look at the outlets for the Survey App Delegate, you will see that the App Delegate has references to both the Root View Controller and the Detail View Controller. You will take advantage of this in your application when you need to communicate back from the Detail View Controller to the Root View Controller.

Now let's take a brief look at the template code for the RootViewController. The RootViewController defaults to a UITableViewController, which the template places in the left pane in landscape mode and the popover in portrait mode. The template code is a straightforward implementation of a table just like you learned in the last chapter. You will find the typical TableView delegate methods numberOfSectionsInTableView:, tableView:numberOfRowsInSection:, and tableView:cellForRowAtIndexPath: implemented in this class.

The DetailViewController is a UIViewController subclass. The template creates an associated XIB file called DetailView.xib. This XIB contains the user interface elements that you will display in the detail view. By default, the XIB contains a label only. In the sample, you will add other controls to allow the user to enter and view survey data.

When the user selects an item in the RootViewController, the Table View calls the tableView:didSelectRowAtIndexPath: method. The default implementation of this method sets the detailItem property of the detailViewController, which, by default, is of type id.

Instead of synthesizing the detailItem property of the DetailViewController, there is code that implements the setter. This is a departure from most of the properties that we have seen so far. The setter is coded because you need to do some additional processing aside from simply setting the associated instance variable. The method sets the detailItem instance variable and then calls the configureView method. The configureView method updates the display to show the new detailItem data. Then, the setter dismisses the popover, if it is visible.

Building the Detail Interface

Now you are ready to start working on the detail view interface. First, however, there are a couple of changes that you need to make to the DetailViewController header file in preparation for the changes that you will make to the view in Interface Builder.

In the DetailViewController header, change the type of the detailItem instance variable to NSDictionary*:

NSDictionary* detailItem;

Also, change the type in the property declaration for the detailItem property:

@property (nonatomic, retain) NSDictionary*  detailItem;

Next, you will add instance variables and outlet properties for the UITextFields that you will use in Interface Builder to collect the survey data. In the @interface section, add the following instance variables:

UITextField* firstNameTextField;
UITextField* lastNameTextField;
UITextField* addressTextField;
UITextField* phoneTextField;
UITextField* ageTextField;
                                                         
Building the Detail Interface

Then, add the associated properties:

@property (nonatomic, retain) IBOutlet UITextField* firstNameTextField;
@property (nonatomic, retain) IBOutlet UITextField* lastNameTextField;
@property (nonatomic, retain) IBOutlet UITextField* addressTextField;
@property (nonatomic, retain) IBOutlet UITextField* phoneTextField;
@property (nonatomic, retain) IBOutlet UITextField* ageTextField;
                                                         
Building the Detail Interface

Next, add the clearSurvey and addSurvey IBAction methods that will execute when the user taps on the Clear or Add buttons in the interface:

-(IBAction)clearSurvey:(id)sender;
-(IBAction)addSurvey:(id)sender;

Because you will not use the detailDescriptionLabel, delete the detailDescriptionLabel instance variable and outlet property.

Now, switch over to the DetailViewController implementation file. Implement the viewDidUnload method to set the properties that hold the user interface controls to nil:

- (void)viewDidUnload {
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.popoverController = nil;

    self.firstNameTextField = nil;
    self.lastNameTextField = nil;
    self.addressTextField = nil;
    self.phoneTextField = nil;
    self.ageTextField = nil;
}
                                                         
Building the Detail Interface

Finally, implement the dealloc method to free the memory that your interface controls use:

- (void)dealloc {
    [popoverController release];
    [toolbar release];

    [detailItem release];
    [firstNameTextField release];
    [lastNameTextField release];
    [addressTextField release];
    [phoneTextField release];
    [ageTextField release];

    [super dealloc];
}
                                                         
Building the Detail Interface

Now you are ready to move on to building the user interface in Interface Builder. Double-click on the DetailView.xib file to open the file in Interface Builder. Delete the UILabel that is in the view by default.

You will be building an interface like the one in Figure 4-5. Add a UILabel for each field in the interface. Change the text to match the text in Figure 4-5. Next, add a UITextField next to each of the labels that you just created. Finally, add two UIButtons at the bottom of the form and change their text to read Clear and Add.

DetailViewController interface

Figure 4.5. DetailViewController interface

Now you need to connect each UITextField in Interface Builder to the appropriate outlet in File's Owner. This will enable you to retrieve the data from these controls or populate the controls with data. Finally, connect the Touch Up Inside event of the Clear button to the clearSurvey: method in File's Owner. Likewise, connect the Touch Up Inside event of the Add button to the addSurvey: method in File's Owner.

Implementing Save and Master/Detail View

In this section, you implement the basic functionality of the application that demonstrates using the split view to show master/detail view relationships.

Setting Up the DetailViewController

First, you will implement stubs for the clearSurvey: and addSurvey: methods in the DetailViewController. These methods will create a log entry in the console when they execute. This will allow you to verify that you have correctly linked the buttons in the user interface to the source code.

The clearSurvey: method should log that the user has invoked the method. Then, it should clear all of the user interface elements. Here is the implementation of the clearSurvey: method:

-(IBAction)clearSurvey:(id)sender
{
    NSLog (@"clearSurvey");
    // Update the user interface for the detail item.
    self.firstNameTextField.text = @"";
    self.lastNameTextField.text = @"";
    self.addressTextField.text = @"";
    self.phoneTextField.text = @"";
    self.ageTextField.text = @"";
}
                                                         
Setting Up the DetailViewController

You will eventually code the addSurvey: method to save the survey data. Because you are not quite ready to do that yet, simply implement the method to log that the user has invoked it by tapping the Add button. Here is the implementation:

-(IBAction)addSurvey:(id)sender
{
    NSLog (@"addSurvey");
}

Build and run your application. When the application starts in the iPad simulator, click each button and verify that you see the correct log statement in the console. This will prove that you have properly connected the buttons to the code.

Now you are ready to modify the configureView method in the DetailViewController to take the data from the detailItem model object and populate the UITextFields that display the data in the user interface. Here is the new implementation of the configureView method:

- (void)configureView {
    // Update the user interface for the detail item.
    self.firstNameTextField.text = [detailItem objectForKey:@"firstName"];
    self.lastNameTextField.text = [detailItem objectForKey:@"lastName"];
    self.addressTextField.text = [detailItem objectForKey:@"address"];
    self.phoneTextField.text = [detailItem objectForKey:@"phone"];
    self.ageTextField.text = [[detailItem objectForKey:@"age"] stringValue];
}
                                                         
Setting Up the DetailViewController

Each survey is stored in an NSDictionary. If you are not familiar with this class, you can use an NSDictonary to store a set of key-value pairs, as long as each key is unique. This method simply calls the objectForKey method on the detailItem dictionary to obtain the value that you will display in the text field. The only wrinkle is that the age field is stored as an NSNumber, so you need to convert the number to a string by calling the stringValue method.

Changes to the RootViewController

Now you need to move on to the RootViewController. Here, you will add an NSMutableArray* to hold the collection of surveys. You cannot simply use an NSArray because you will need to be able to add surveys to the collection on-the-fly. Remember, NSArray is immutable, meaning that once you have created it, you cannot modify it.

In the RootViewController header file, add an instance variable called surveyDataArray of type NSMutableArray*:

NSMutableArray* surveyDataArray;

Add a property for your new surveyDataArray:

@property (nonatomic, retain) NSMutableArray* surveyDataArray;

Switch over to the RootViewController implementation file and modify the @synthesize statement to synthesize your new surveyDataArray property:

@synthesize detailViewController,surveyDataArray;

Modify the viewDidUnload and dealloc methods to clean up your new property:

- (void)viewDidUnload {
    // Relinquish ownership of anything that can be recreated in viewDidLoad or
    // on demand.
    // For example: self.myOutlet = nil;
    self.surveyDataArray = nil;
}


- (void)dealloc {
    [detailViewController release];
    [surveyDataArray release];
    [super dealloc];
}
                                                         
Changes to the RootViewController

Finally, modify the viewDidLoad method to create and initialize the NSMutableArray:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.clearsSelectionOnViewWillAppear = NO;
    self.contentSizeForViewInPopover = CGSizeMake(320.0, 600.0);

    NSMutableArray* array = [[NSMutableArray alloc] init];
    self.surveyDataArray = array;
    [array release];
}
                                                         
Changes to the RootViewController

Modify the TableView Methods

The next step is to modify the Table View datasource methods to use the surveyData array as the UITableView's datasource. First, you will need to update the tableView:numberOfRowsInSection: method to return the number of items in the array, like this:

- (NSInteger)tableView:(UITableView *)aTableView
    numberOfRowsInSection:(NSInteger)section {
    // Return the number of rows in the section.
    return [self.surveyDataArray count];
}
                                                         
Modify the TableView Methods

Now, you should change the tableView:cellForRowAtIndexPath: method to get data from the surveyDataArray to use as the text in the table cells:

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

    static NSString *CellIdentifier = @"CellIdentifier";

    // Dequeue or create a cell of the appropriate type.
    UITableViewCell *cell =
        [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc]
                 initWithStyle:UITableViewCellStyleDefault
                 reuseIdentifier:CellIdentifier] autorelease];
        cell.accessoryType = UITableViewCellAccessoryNone;
    }

    // Configure the cell.
    NSDictionary* sd = [self.surveyDataArray objectAtIndex:indexPath.row];

    cell.textLabel.text = [NSString stringWithFormat:@"%@, %@",
                           [sd objectForKey:@"lastName"],
                           [sd objectForKey:@"firstName"]];
    return cell;
}
                                                         
Modify the TableView Methods

In this method, you get an NSDictionary object from the surveyDataArray with an index based on the row that the table has requested. Then, you get the lastName and firstName strings from the dictionary and display them in the cell's textLabel.

The last thing that you need to do in the Table View methods is to update the tableView:didSelectRowAtIndexPath: method. In this method, you need to set the detailItem in the detailViewController to the NSDictionary that you retrieve from the surveyDataArray. This passes the data that you want to display, the survey that the user has chosen, to the DetailViewController. Remember that the setter for the detailItem property contains code; it is not just a synthesized property. This code calls the configureView method that updates the view to display the record that the user has chosen. Here is the code for the tableView:didSelectRowAtIndexPath: method:

- (void)tableView:(UITableView *)aTableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    /*
     When a row is selected, set the detail View Controller's detail item to the
     item associated with the selected row.
     */
    [aTableView deselectRowAtIndexPath:indexPath animated:NO];

    NSDictionary* sd = [self.surveyDataArray objectAtIndex:indexPath.row];
    detailViewController.detailItem = sd;
}
                                                         
Modify the TableView Methods

Adding Surveys

To complete this example, you will add code to the project to enable the user to add surveys to the application. In the RootViewController header, you will need to declare a method to add a survey. You should call this method addSurveyToDataArray and the signature should look like this:

-(void) addSurveyToDataArray: (NSDictionary*) sd;

Recall that you are using an NSDictionary to hold the data for each survey. Therefore, the addSurveyToDataArray method accepts an NSDictionary* that holds the survey data that you want to add to the array of completed surveys.

Next, switch over to the RootViewController implementation. You should implement the addSurveyToDataArray function as follows:

-(void) addSurveyToDataArray: (NSDictionary*) sd
{
    NSLog (@"addSurveyToDataArray");

    // Add the survey to the results array
    [self.surveyDataArray addObject:sd];

    // Refresh the tableview
    [self.tableView reloadData];
}
                                                         
Adding Surveys

This method simply adds the dictionary object that it receives to the surveyDataArray. Then, because the user has modified the data for the Table View, you need to tell the Table View to reload its data in order to refresh the display and show the new survey.

Now, you need to move over to the DetailViewController to implement the addSurvey method. This method runs when the user taps the Add button in the user interface. First, however, you will need to add #import statements to the DetailViewController header to import the RootViewController and SurveyAppDelegate headers:

#import "RootViewController.h"
#import "SurveyAppDelegate.h"
                                                         
Adding Surveys

Now you can move into the DetailViewController implementation to implement the addSurvey method:

-(IBAction)addSurvey:(id)sender
{
    NSLog (@"addSurvey");

    // Create a new NSDictionary object to add to the results array of the root
    // View Controller

    // Set the values for the fields in the new object from the text fields of
    // the form

    NSArray *keys = [NSArray arrayWithObjects:@"firstName", @"lastName",
                     @"address", @"phone", @"age", nil];
    NSArray *objects = [NSArray arrayWithObjects:self.firstNameTextField.text,
                        self.lastNameTextField.text,
                        self.addressTextField.text,
                        self.phoneTextField.text,
                        [NSNumber
                          numberWithInteger:[ self.ageTextField.text intValue]],
                        nil];

    NSDictionary* sData = [[NSDictionary alloc]
                           initWithObjects:objects forKeys:keys];


    // Get a reference to the app delegate so we can get a reference to the
    // Root View Controller
    SurveyAppDelegate* appDelegate =
        [[UIApplication sharedApplication] delegate];
    RootViewController* rvc = appDelegate.rootViewController;

    // Call the addSurveyToDataArray method on the rootViewController to
    // add the survey data to the list
    [rvc addSurveyToDataArray:sData];

    // Clean up
    [sData release];

}
                                                         
Adding Surveys

In this method, you need to build an NSDictionary that you will pass to the RootViewController to add to the surveyDataArray. To create the dictionary, you first create two NSArrays, one for the keys of the dictionary and one for the related objects. The keys can be any arbitrary NSStrings. You obtain the objects for the dictionary from the UITextField objects that you added in Interface Builder. Once you have built your arrays, you create an NSDictionary by passing in the objects array and the keys array.

Next, you need to call the addSurveyToDataArray method on the RootViewController. If you remember, the RootViewController holds a reference to the DetailViewController. However, the DetailViewController does not hold a reference to the RootViewController. There are a couple of ways that you can remedy this. For this example, I have chosen to simply get a reference to the RootViewController from the SurveyAppDelegate.

You can get a reference to the app delegate at any time by calling the delegate method on the application's UIApplication object. UIApplication is the core object at the root of all iPhone and iPad applications. UIApplication is a singleton class to which you can obtain a reference by calling the sharedApplication method. Once you obtain a reference to the application, you can call its delegate method to get a reference to your application delegate. In this case, the delegate holds references to both the RootViewController and the DetailViewController. The code then goes on to get a reference to the RootViewController from the application delegate. Finally, you call the addSurveyToDataArray method on the RootViewController to add the newly created survey dictionary to the array.

You are now ready to build and run the application. You should be able to add new surveys and see them appear in the left hand pane in portrait mode. Select an item in the list and you will see the display in the right-hand pane change to the item you selected. Rotate the device to see how the UISplitViewController behaves in both landscape and portrait modes.

DISPLAYING DATA IN A POPOVER

Another user interface element that is new and unique to the iPad is the UIPopoverController. You can use this controller to display information on top of the current view. This allows you to provide context-sensitive information on top of the main application data without swapping views as would be required in an iPhone application. The UISplitViewController uses a UIPopoverController to display the master data list when the device is in portrait orientation and the user taps the button to disclose the master list. For example, when your Survey application is in portrait orientation and the user taps the Root List button, the RootViewController is displayed in a UIPopoverController.

Another interesting feature of the popover is that the user can dismiss it by simply tapping outside its bounds. Therefore, you can use this controller to display information that might not necessarily require any user action. In other words, the user does not have to implicitly accept or cancel any action to dismiss the popover.

The UIPopoverController displays a UIViewController as its content. You can build the content view in any way that you wish. You should, however, consider the size of the popover as you are building the view that it will contain. You should also consider the position in which you want to show the popover. You should display a popover next to the user interface element that displayed it. When presenting a popover, you can either attach it to a toolbar button or provide a CGRect structure to give the popover a reference location. Finally, you can specify the acceptable directions for the arrow that points from the popover to the reference location. You should generally permit UIKit to control the location of the popover by specifying UIPopoverArrowDirectionAny as the permitted direction for the arrow.

In this section, you will create a UIPopoverController and display it in the Survey application. The popover will simply display a new UIViewController that will show some helpful information on performing surveys, as you can see in Figure 4-6.

The Survey Application Informational Popover

Figure 4.6. The Survey Application Informational Popover

Building the InfoViewController

The UIPopoverController is a container that you can use to display another View Controller anywhere on the screen on top of another view. Therefore, the first thing that you need to do when you want to display content in the popover is build the View Controller that you would like to display. For the Survey application, you will create a new class called InfoViewController. Then, you will display this class in a UIPopoverController when the user taps an informational button.

The first step is to create your new class. Add a new UIViewController subclass called InfoViewController to your project. As you add the class, make sure that the "Targeted for iPad" and "With XIB for user interface" checkboxes in the New File dialog box are selected.

Open your new InfoViewController header file. Add a UILabel* instance variable called infoLabel. Then, add an IBOutlet property that you can use to set and get your new instance variable. Finally, add a method signature for a new method called setText. You will use the setText method to set the text that you want to display in the popover. The header file for the InfoViewController should look like this:

#import <UIKit/UIKit.h>


@interface InfoViewController : UIViewController {
  UILabel* infoLabel;
}

@property (nonatomic, retain) IBOutlet UILabel* infoLabel;
-(void) setText: (NSString*) text;

@end
                                                         
Building the InfoViewController

Next, you will open the InfoViewController XIB file with Interface Builder to build the user interface for the new View Controller. Double-click on the InfoViewController XIB file to open it in Interface Builder. For this application, the user interface will be extremely simple, just a lone UILabel. However, you can build views that are as complex as you want with Interface Builder and use the methodology outlined here to display those interfaces using popovers.

Once you have the XIB file opened in Interface Builder, open the Attributes inspector for the View by pressing Command+1 or selecting Tools

Building the InfoViewController

Now that you have the view configured correctly, add a UILabel control and resize it to fill most of the view. The exact size and position are not particularly important for this example.

Finally, connect the new UILabel to the infoLabel property of File's Owner. This connects your code to the interface that you built in Interface Builder. You can now save your InfoViewController XIB file and close Interface Builder.

Now, you will need to add some logic to the InfoViewController implementation file. First, you should add code to the viewDidUnload and dealloc methods to clean up the instance variable and property of the class:

- (void)viewDidUnload {
    [super viewDidUnload];

    self.infoLabel = nil;

    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}


- (void)dealloc {
    [infoLabel release];

    [super dealloc];
}
                                                         
Building the InfoViewController

Next, add a line to synthesize the infoLabel property:

@synthesize infoLabel;

The next step is to implement the viewDidLoad method. In this method, you will set the size of the View Controller to display in the popover. Here is the code:

- (void)viewDidLoad {
    [super viewDidLoad];

    CGSize size;
    size.width=320;
    size.height = 175;
    self.contentSizeForViewInPopover = size;
}
                                                         
Building the InfoViewController

The original size of the popover comes from the View Controller's contentSizeForViewInPopover property. The default size for a UIPopoverController is 320 pixels by 1100 pixels. There are two ways that you can change the size of the popover. First, you can set the size in the View Controller that the popover will hold by setting the View Controller's size using the contentSizeForViewInPopover property. Alternatively, you can set the size of the popoverContentSize property of the Popover Controller. Keep in mind that if you use this method and change the View Controller that you are displaying in the popover, the popover will automatically resize to the size of the new View Controller. The custom size that you set in the popoverContentSize property is lost.

In this example, you created a CGSize struct to define the size of the popover in the View Controller. You set the size to 320 pixels wide by 175 pixels high. Then, you set the contentSizeForViewInPopover property on the View Controller.

Finally, you need to implement the setText method that clients who want to display the InfoViewController will use to set the text to display. This method does nothing more than set the text in the UILabel:

-(void) setText: (NSString*) text
{
    self.infoLabel.text = text;
}
                                                         
Building the InfoViewController

Displaying the UIPopoverController

Now that you have a View Controller, you need to display it in a popover. You will display the popover when the user taps a button in the DetailViewController. Therefore, you will need to make some changes to the DetailViewController header file. First, add a #import statement for the new InfoViewController.h header file:

#import "InfoViewController.h"

Next, you will add a new action method called showInfo. The user will invoke this method when he taps on the information button in the user interface. Here is the declaration:

-(IBAction)showInfo:(id)sender;

The next step is to add a new outlet and instance variable for the UIButton* called infoButton. Inside of the interface declaration, add the instance variable for the UIButton*:

UIButton* infoButton;

Outside of the interface declaration, declare the infoButton property:

@property (nonatomic, retain) IBOutlet UIButton* infoButton;

Now add an instance variable for the UIPopoverController. Call it infoPopover:

UIPopoverController *infoPopover;

Add a property for the infoPopover:

@property (nonatomic, retain) UIPopoverController *infoPopover;

Now you need to move over to the DetailViewController implementation file.

In the implementation, synthesize the new infoButton and infoPopover properties:

@synthesize infoButton,infoPopover;

Next, you will implement the showInfo method to show the InfoViewController in the infoPopover:

-(IBAction)showInfo:(id)sender
{
    NSLog (@"showInfo");

    // Instatiate Info View Controller
    InfoViewController *ivc = [[InfoViewController alloc] init];

    UIPopoverController *popover =
        [[UIPopoverController alloc] initWithContentViewController:ivc];

    [ivc setText:@"If the survey taker refuses, that is okay"];

    [ivc release];

    // Set the infoPopover property
    self.infoPopover = popover;

    // Clean up the local popover
    [popover release];

    [self.infoPopover presentPopoverFromRect:self.infoButton.frame
                                      inView:self.view
                    permittedArrowDirections:UIPopoverArrowDirectionAny
                                    animated:YES];

}
                                                         
Displaying the UIPopoverController

The first thing that you do in this method, after logging the method name, is create an instance of the InfoViewController.

Next, you allocate a UIPopoverController and initialize it by calling the initWithContentViewController method. You pass the View Controller that you want to display in the popover as a parameter to this method. Next, you set the text that you want to display in the popover and release the InfoViewController. You must send the InfoViewController the release message or you will leak memory. When you pass the View Controller in to the popover in the initWithContentViewController method, the popover will retain the View Controller.

The next step is to set the infoPopover property to the new popover that you created in this method. Then, you can release the local popover.

Finally, you call the presentPopoverFromRect:inView:permittedArrowDirections:animated: method to present the popover. The first parameter to this method specifies the CGRect structure from which the popover should emanate. In this example, you use the frame of the info button that you will add to the user interface. Then, you specify the view that will hold the popover. The permittedArrowDirections parameter allows you to specify from which direction the popover will appear with respect to the frame that you specified in the first parameter. Generally, you should specify any direction as permitted and leave the layout and positioning of the popover to the UIKit framework. The permitted directions are specified using members from the UIPopoverArrowDirection enumeration. Valid direction values are:

  • UIPopoverArrowDirectionUp

  • UIPopoverArrowDirectionDown

  • UIPopoverArrowDirectionLeft

  • UIPopoverArrowDirectionRight

  • UIPopoverArrowDirectionAny

  • UIPopoverArrowDirectionUnknown

Finally, the last parameter specifies whether the framework should animate the display of the popover.

The last step is to clean up the infoButton and infoPopover properties in the dealloc and viewDidUnload methods:

- (void)viewDidUnload {
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.popoverController = nil;

    self.firstNameTextField = nil;
    self.lastNameTextField = nil;
    self.addressTextField = nil;
    self.phoneTextField = nil;
    self.ageTextField = nil;

    self.infoButton = nil;
    self.infoPopover = nil;
}

- (void)dealloc {
    [popoverController release];
    [toolbar release];

    [detailItem release];
    [firstNameTextField release];
    [lastNameTextField release];
    [addressTextField release];
    [phoneTextField release];
    [ageTextField release];
    [infoButton release];
    [infoPopover release];
    [super dealloc];
}
                                                         
Displaying the UIPopoverController

Now, you need to add the info button to the user interface in Interface Builder. Open the DetailView.xib file in Interface Builder. Add a new UIButton next to the age UITextField. In the attributes inspector for the button, change the type of the button to Info Dark. This will change the button from a rounded rectangle to a gray circle with a white letter i inside.

Next, wire the File's Owner infoButton property to the new button. Then, wire the infoButton's touchUpInside event to the File's Owner showInfo method.

You are finished with the interface so you can save the XIB file and close Interface Builder.

Build and run the application. When the interface appears, tap the new info button. You should see the message appear in a popover next to the info button.

GESTURE RECOGNIZERS

The basic UIKit controls such as the UIButton can detect simple user interactions such as pressing down on the button and lifting a finger up. You can see these events in Interface Builder by selecting a UIButton and opening the Connections inspector.

Before iPhone OS 3.2, if your application required a more complex behavior such as recognizing swipe or pinch gestures, you had to implement these features yourself. This would involve writing your own code to examine the stream of user touches and the implementation of heuristics algorithms to determine if the user was performing the gestures. To assist developers, Apple introduced the concept of Gesture Recognizers in iPhone OS 3.2. You can use gesture recognizers on both the iPhone and the iPad as long as the device that you are targeting is running at least iOS 3.2.

The UIGestureRecognizer Class

The UIGestureRecognizer class is an abstract base class that defines what gesture recognizer classes must do and how they should operate. Apple has provided the concrete subclasses UIPinchGestureRecognizer, UIPanGestureRecognizer, and UISwipeGestureRecogsnizer that you can use in your applications to recognize pinch, pan, and swipe gestures respectively. This saves you from having to write the significant amount of code required to interpret a series of touches as one of these common gestures. However, if your application requires gestures that the framework does not support, you can implement your own custom gesture recognizer as a UIGestureRecognizer subclass to define any gesture that your application may need.

To use a gesture recognizer, you must attach the gesture recognizer to a view. When you attach a gesture recognizer to a view, the framework routes touches in the application to the gesture recognizer before sending them to the view. This gives the gesture recognizer the chance to evaluate the touches to see if they qualify as a gesture. If the touches meet the requirements of a gesture, the framework cancels the touch messages and instead of sending the touches to the view, the framework sends a gesture message instead. If the touches do not qualify as a gesture, the framework sends the touches to the view.

There are two different types of gestures, discrete and continuous. A discrete gesture, such as a tap, causes the gesture recognizer to simply send one action message when the action is complete. A continuous gesture, like a pinch, results in the gesture recognizer calling the action message multiple times until the continuous action is completed.

You implement a gesture recognizer in your code by instantiating a gesture recognizer concrete class. This can be one of the Apple provided classes mentioned above, or your own custom subclass of UIGestureRecognizer. Then, you assign a target and an action to the recognizer. The target is the class that will receive the action and the action is the method that the gesture recognizer will call when a gesture is recognized. Finally, you attach the gesture recognizer to the view in which you want gestures recognized.

In this section, you will add gesture recognizers to your Survey application. You will use a UISwipeGestureRecognizer to determine if the user has swiped across the DetailViewController screen. If he has, you will navigate either forward or backward in the survey list, depending on the direction of the swipe, and display the appropriate record.

Using Gesture Recognizers

The first step in using a gesture recognizer is to create an instance of the recognizer that you want to use and attach it to a view. You will do this in the viewDidLoad method of the DetailViewController like this:

- (void)viewDidLoad {
    [super viewDidLoad];

    // Create the right swipe gesture recognizer
    UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc]
        initWithTarget:self action:@selector(handleSwipeRight:)];

    swipeRight.direction=UISwipeGestureRecognizerDirectionRight;

    // Attach it to the view
    [self.view addGestureRecognizer:swipeRight];


    // Create the left swipe gesture recognizer
    UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc]
        initWithTarget:self action:@selector(handleSwipeLeft:)];

    swipeLeft.direction=UISwipeGestureRecognizerDirectionLeft;

    // Attach it to the view
    [self.view addGestureRecognizer:swipeLeft];


    // Clean up
    [swipeRight release];
    [swipeLeft release];
}
                                                         
Using Gesture Recognizers

First, you call the superclass viewDidLoad method to ensure that the class is set up correctly.

Next, you create an instance of the UISwipeGestureRecognizer. In creating this instance, you need to specify the target class that will receive the action method call when the gesture recognizer detects a swipe. The action parameter specifies the method that the gesture recognizer should call when it detects the gesture. In this case, the target is self and the method the gesture recognizer will call is handleSwipeRight, which you will implement in a moment.

After you create an instance of the UISwipeGestureRecognizer, you need to specify which swipe direction you want to detect. Here, you have specified that you would like to detect swipes to the right.

Finally, you add the gesture recognizer to the view by calling the view's addGestureRecognizer method, passing in the gesture recognizer.

The code then goes on to perform the same procedure to attach another swipe gesture recognizer to the view. This time, you configure the recognizer to detect swipes to the left. You have also set this gesture recognizer to call the handleSwipeLeft method as its action.

Lastly, you release both of the gesture recognizers that you created in this method.

In the DetailViewController header, add action methods for the gesture recognizers handleSwipeRight and handleSwipeLeft:

-(void)handleSwipeRight:(UIGestureRecognizer*) sender;
-(void)handleSwipeLeft:(UIGestureRecognizer*) sender;
                                                         
Using Gesture Recognizers

In order to be able to implement the functionality to navigate forward and backward in the survey array using gestures, you have to add a variable and a couple of methods to the RootViewController. In the RootViewController header file, add an instance variable of type int called currentIndex. You will use this variable to maintain the index of the record that you are displaying in the DetailViewController. Here is the declaration:

int currentIndex;

You also need to add declarations for two new methods, moveNext and movePrevious. The gesture recognizer action methods will call these methods on the RootViewController to tell the RootViewController to navigate to the next or previous record. Here are the declarations for the moveNext and movePrevious methods:

-(void) moveNext;
-(void) movePrevious;
                                                         
Using Gesture Recognizers

In the RootViewController implementation, you will need to add code to the viewDidLoad method to initialize the currentIndex instance variable:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.clearsSelectionOnViewWillAppear = NO;
    self.contentSizeForViewInPopover = CGSizeMake(320.0, 600.0);
// Set up surveyDataArray
    NSMutableArray* array = [[NSMutableArray alloc] init];
    self.surveyDataArray = array;

    // Initialize the current index
    currentIndex = 0;
}
                                                         
Using Gesture Recognizers

Next, you need to modify the tableView:didSelectRowAtIndexPath: method to set the currentIndex instance variable to the row that the user has selected in the table:

- (void)tableView:(UITableView *)aTableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    /*
     When a row is selected, set the detail View Controller's detail item to the
     item associated with the selected row.
     */
    [aTableView deselectRowAtIndexPath:indexPath animated:NO];

    NSDictionary* sd = [self.surveyDataArray objectAtIndex:indexPath.row];
    detailViewController.detailItem = sd;

    // Set the currentIndex
    currentIndex = indexPath.row;

}
                                                         
Using Gesture Recognizers

Finally, you will implement the moveNext and movePrevious methods:

-(void) moveNext
{
    NSLog (@"moveNext");
    // Check to make sure that there is a next item to move to
    if (currentIndex < (int)[self.surveyDataArray count] −1)
    {
NSDictionary* sd = [self.surveyDataArray objectAtIndex:++currentIndex];
        detailViewController.detailItem = sd;
    }
}

-(void) movePrevious
{
    NSLog (@"movePrevious");
    // Check to make sure that there is a previous item to move to
    if (currentIndex > 0)
    {

        NSDictionary* sd = [self.surveyDataArray objectAtIndex:--currentIndex];
        detailViewController.detailItem = sd;
    }

}
                                                         
Using Gesture Recognizers

In the moveNext method, you first check to make sure that the next record exists. If it does, you increment the currentIndex, get the object at the new index from the surveyDataArray, and set the detailItem of the DetailViewController to the corresponding NSDictionary.

The movePrevious method is the same with one minor exception. You need to test to make sure that the user is not trying to navigate backward if the current index is already 0.

Back in the DetailViewController implementation, you are ready to implement the handleSwipeRight and handleSwipeLeft methods:

-(void)handleSwipeRight:(UIGestureRecognizer*) sender
{
    NSLog (@"handleSwipeRight");
    // Get a reference to the app delegate so we can get a reference to the
    // Root View Controller
    SurveyAppDelegate* appDelegate =
        [[UIApplication sharedApplication] delegate];
    RootViewController* rvc = appDelegate.rootViewController;

    // Call the movePrevious method on the rootViewController to move to the
    // previous survey in the list
    [rvc movePrevious];
}

-(void)handleSwipeLeft:(UIGestureRecognizer*) sender
{
    NSLog (@"handleSwipeLeft");
    // Get a reference to the app delegate so we can get a reference to the
    // Root View Controller
    SurveyAppDelegate* appDelegate =
        [[UIApplication sharedApplication] delegate];
    RootViewController* rvc = appDelegate.rootViewController;

    // Call the moveNext method on the rootViewController to move to the
    // next survey in the list
    [rvc moveNext];
}
                                                         
Using Gesture Recognizers

In the handleSwipeRight and handleSwipeLeft methods, you do almost the same thing. First, you get a reference to the app delegate so that you can get a reference to the RootViewController. Then, you call the appropriate method, either moveNext or movePrevious, on the RootViewController.

Build and run the application. You should be able to add new surveys to the application. Then, select a row, or just start with the first row. Swipe right in the DetailViewController and the application should display the data in the DetailViewController for the next entry in the completed survey list. Swiping to the left should display the detail information for the previous entry in the list.

FILE SHARING SUPPORT

Sometimes, when building applications, you will create data on the device that you want to share with the desktop or vice versa. In iPhone SDK 3.2, Apple introduced the capability of sharing files between the desktop and the device through iTunes. If you have purchased the Pages, Numbers, or Keynote application for the iPad, you have seen that you can create documents on your computer and make them available on the iPad. Conversely, you can create new documents on the iPad and they are available in iTunes to move onto the computer.

Using the file sharing support with the 3.2 iPhone SDK, developers can make the contents of the application's /Documents directory available to the user on the computer. Keep in mind that file-sharing support does not enable sharing of documents between applications on the device.

Enabling file sharing is simple. You need only make a change to the Info.plist file that you deploy with your application. In this section, you will add code to the Survey application that will store the completed surveys as an XML file. Then, using file sharing, you will see that you can access this file on the computer after syncing through iTunes.

Enable File Sharing in the Sample Application

First, you need to add a new key, UIFileSharingEnabled, to the Survey-Info.plist file. This file is located in the Resources folder of your Xcode project. You can add this key inside of Xcode by first selecting the Survey-Info.plist file in the browser pane. You should see the plist file displayed in the right hand pane. Click in the right hand margin next to any of the plist entries and you will see a plus sign appear. Click the plus sign to add a new entry to the plist. In the dropdown list that appears, select "Application supports iTunes file sharing." To the right of the key, you will then see a box with a checkmark in it. Leave the box checked to enable file sharing.

That is all that you need to do to enable file sharing.

Serializing the Survey Data Array

Next, you will add code to serialize the surveyDataArray and store the serialized file in the /Documents folder for the application.

When the user installs an application on an iPhone or iPad, the installation process creates a home directory for that application. Each application has its own home directory. You should write your application data files to the /Documents directory. Additionally, iTunes backs up this directory when the user syncs the device with a computer.

Now that you understand where to save the application data, you will add code to the applicationWillTerminate method in the SureveyAppDelegate to serialize the array and save it to a file. Serialization simply takes a data structure that is stored in memory, in this case an array, and converts it to a format that can be saved to a file or sent over a network. Here is the implementation:

- (void)applicationWillTerminate:(UIApplication *)application {
    // Serialize the rootViewController's surveyDataArray
    NSData *serializedData;
    NSString *error;

    serializedData = [NSPropertyListSerialization
                      dataFromPropertyList:rootViewController.surveyDataArray
                      format:NSPropertyListXMLFormat_v1_0
                      errorDescription:&error];

    if (serializedData)
    {
        // Serialization was successful, write the data to the file system
        // Get an array of paths.
        // (This function is carried over from the desktop)
        NSArray *documentDirectoryPath =
            NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                NSUserDomainMask, YES);
        NSString *docDir = [NSString stringWithFormat:@"%@/serialized.xml",
                            [documentDirectoryPath objectAtIndex:0]];

        [serializedData writeToFile:docDir atomically:YES];
    }
    else
    {
        // An error has occurred, log it
        NSLog(@"Error: %@",error);
    }
}
                                                         
Serializing the Survey Data Array

First, you serialize the data in the surveyData array by using the NSPropertyListSerialization class. This class provides methods that covert the types of objects that you can use in a property list (NSData, NSString, NSArray, NSDictionary, NSDate, and NSNumber) into different serialized forms. In this case, you will convert an NSArray into XML format. I chose XML for this example so that you could open the file on your computer after syncing with iTunes and verify that the data in the file is the same as the data that is contained in the application. For production applications, it is generally more efficient to use the binary format.

After serializing the array to XML, the code obtains the path to the /Documents directory for the application. Here, you use the NSSearchPathForDirectoriesInDomains function to obtain the path to the /Documents directory by passing in the NSDocumentDirectory constant as the directory parameter. The NSSearchPathDirectory enumeration provides several predefined constants that you can use to help you to navigate to specific directories.

Next, you use the path to the /Documents directory to build a string that represents the file that you want to save. Finally, you call the writeToFile method of the serializedData object to write the XML to a file.

Deserializing and Loading the Survey Data Array

Now that you have written code to save the survey data array to disk, you need to add code to load the array from disk when the application starts. The process of taking the data from its on-disk format and turning it back into an object is called deserialization. You will do that in the RootViewController's viewDidLoad method. Modify the viewDidLoad method to use the data from the plist, if it exists, like this:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.clearsSelectionOnViewWillAppear = NO;
    self.contentSizeForViewInPopover = CGSizeMake(320.0, 600.0);

    // Set up surveyDataArray
    // Get an array of paths. (This function is carried over from the desktop)
    NSArray *documentDirectoryPath =
    NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                        NSUserDomainMask, YES);
    NSString *docDir = [NSString stringWithFormat:@"%@/serialized.xml",
                        [documentDirectoryPath objectAtIndex:0]];
    NSData* serializedData = [NSData dataWithContentsOfFile:docDir];

    // If serializedData is nil, the file doesn't exist yet
    if (serializedData == nil)
    {
        NSMutableArray* array = [[NSMutableArray alloc] init];
        self.surveyDataArray = array;
        [array release];
    }
    else
    {
        // Read data from the file
        NSString *error;

        self.surveyDataArray =
        (NSMutableArray *)[NSPropertyListSerialization
                           propertyListFromData:serializedData
                           mutabilityOption:kCFPropertyListMutableContainers
                           format:NULL errorDescription:&error];
    }

    // Initialize the current index
    currentIndex = 0;
}
                                                         
Deserializing and Loading the Survey Data Array

This code is the opposite of the code that you wrote in the previous section. First, you create the file name the same way that you did before using the NSSearchPathForDirectoriesInDomains function. Then, you create an NSData object from the serialized file by calling the dataWithContentsOfFile method.

Next, you need to check if the serialized data exists. If there is no data, you need to create a new NSMutableArray* to hold the completed surveys. If there was data, you populate the surveyDataArray from the NSData object by calling the propertyListFromData:mutabilityOption:format:errorDescription: method of the NSPropertyListSerialization class.

Sharing the Data

Now you are ready to build and run the application on an iPad device. Add some survey data and then quit the application.

To retrieve the data from the iPad, hook your iPad up to your computer and sync it with iTunes. Click on the iPad under Devices in iTunes and select the Apps tab. If you scroll to the bottom, you should see Survey listed as one of the applications in the Apps box, as in Figure 4-7. Click on Survey and you should see serialized.xml in the Survey Documents window. Click on serialized.xml and click the Save to button. Save the file to your desktop or some other convenient location.

Survey application and data in iTunes

Figure 4.7. Survey application and data in iTunes

You should be able to navigate to that XML file using the Finder. If you open the XML file, you should see an XML representation of your data. The XML for the sample data looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/
    PropertyList-1.0.dtd">
<plist version="1.0">
<array>
    <dict>
        <key>address</key>
        <string>123 Any St.</string>
        <key>age</key>
        <integer>20</integer>
        <key>firstName</key>
        <string>John</string>
        <key>lastName</key>
        <string>Smith</string>
        <key>phone</key>
        <string>555-1234</string>
    </dict>
    <dict>
        <key>address</key>
        <string>789 Town St.</string>
        <key>age</key>
        <integer>40</integer>
        <key>firstName</key>
        <string>Doris</string>
        <key>lastName</key>
        <string>Jones</string>
        <key>phone</key>
        <string>555-1234</string>
    </dict>
</array>
</plist>

MOVING FORWARD

In this chapter, you learned how to use some of the features of the iPhone SDK that are specific to the iPad. First, you explored using the UISplitViewController to build master/detail displays. Then, you learned how to display informational messages on top of another view using the UIPopoverController. Next, you discovered how to use gesture recognizers to handle complex user interactions. Finally, you found out how you can share data between the device and the computer by using file-sharing support.

This concludes Part I, where you learned how to build a simple data-based application, how to get data onto the device and store it using SQLite, and how to display data and customize the display of data using the UITableView.

In Part II, you will learn about the Core Data framework. Core Data is a powerful library that you can use to create and manage data on the iPhone. Core Data comes with a powerful modeling tool that you will use to help you define the data model for your applications.

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

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