iOS App Development For Dummies (2014)

Part V. Adding the App Content

Chapter 20. Selecting a Destination

In This Chapter

arrow Finding an address for a map coordinate and displaying it on the map

arrow Finding the map coordinate from an address and displaying it on the map

In this chapter, you are down to the final parts needed for the RoadTrip app to be complete. Back in Chapter 11, you added multiple destinations to the Destinations.plist, and now it would be nice if the user could select any of the ones you added.

Providing the user with the ability to select a destination is what you implement in this chapter. You also discover more about Table views along the way. I also show you how to work with modal controllers (which present views that require the user to do something) by creating your own protocol.

The Plan

You’re going to add a new view controller that manages a modal Table view that allows the user to select a destination — such as New York or San Francisco. Figure 20-1 shows the results for both the iPad and iPhone.

image

Figure 20-1: The Destinations modal Table view on both the iPad and iPhone.

Setting Up the DestinationController for the iPad Storyboard

If you’ve followed along throughout this book, by now you should know the drill. As you might expect, you need a view controller to implement the Selecting a Destination interface.

Adding the custom view controller

Follow these steps to add a new Objective-C DestinationController class to the RoadTrip project.

1.     In the Project navigator, select the View Controller Classes group and then either right-click the selection and choose New File from the menu that appears or choose File⇒New⇒File from the main menu (or press image+N).

Whatever method you choose, you're greeted by the New File dialog.

2.     In the left column of the dialog, select Cocoa Touch under the iOS heading, select the Objective-C class template in the top-right pane, and then click Next.

You’ll see a dialog that will enable you to choose the options for your file.

3.     Enter DestinationController in the Class field, enter or choose UIViewController from the Subclass Of drop-down menu, make sure that the Target for iPad check box is selected and that With XIB for User Interface is deselected, and then click Next.

4.     In the Save sheet that appears, click Create.

The Destination controller will be using a Table view, but it won’t use a Table View Controller class. That’s because I show you how to use a Table view with dynamically generated cells (as well as cell selection handled by the controller) as only one element in the view. This is a handy thing to know if you want to take advantage of the power of a Table view without letting a Table view take over the entire screen.

Setting up the DestinationController in the Main_iPad.storyboard

Now that you have a custom view controller, you need to tell the storyboard to load your custom view controller rather than a UIViewController. Follow these steps:

1.     In the Project navigator, select the Main_iPad.storyboard file, and in the Document Outline, select View Controller in the View Controller – Destination Scene.

2.     image Open the Identity inspector in the Utility area using the Inspector selector bar and then choose DestinationController from the Custom Class section’s Class drop-down menu.

Now when Destination is selected in the Master View controller, DestinationController will be instantiated and initialized and will receive events from the user and connect the view to the Trip model.

In Chapter 14, you left this segue style as modal, and I said I would explain a little more about that in Chapter 20. Well, here we are.

3.     Select the segue to the Destination controller on the Canvas.

4.     Select the Attributes inspector for the Inspector selector bar.

A modal dialog requires the user to do something (tap a Table View cell or the Cancel button, for example) before returning to the app.

When you have a modal segue, you can choose a transition style.

5.     Choose Flip Horizontal in the Transition pop-up menu in the Attributes inspector for the segue.

Actually, you can select whatever transition you’d like, but I’d go for Flip Horizontal.

Make sure that Form Sheet is selected in the Presentation pop-up menus. The Presentation choices include

·        Full Screen: The modal view covers the screen.

·        Page Sheet: The height and width are set to the height and width of the screen in Portrait orientation, with the background view dimmed.

·        Form Sheet: The width and height of the modal view are smaller than those of the screen, with the modal view centered on the screen and the background view dimmed.

·        Current Context: The modal view is the same style as its presenting view controller. But if the presenting view controller is in a popover, you can use this presentation style only when the transition style is UIModalTransitionStyleCoverVertical. If not, you’ll get an exception.

After you have the Presentation and the Transition selected in the Attributes inspector, you can get on with formatting the Destination Controller view, which will have a Table view, a Label view, and a very spiffy image as well after you follow these steps:

1.     Select the Destination controller in the storyboard Canvas and then drag in a Navigation bar from the Library.

You’re going to need someplace to put the Cancel button. Place the Navigation bar at the top of the view.

2.     In the Navigation bar on the Canvas, select the Title (field). Still in the Attributes inspector, enter Destinations in the Title field for the selected element (Navigation bar, in this case).

3.     Drag a Bar Button item from the Library and place it on the left side of the Navigation bar on the Canvas.

4.     Choose Cancel from the Identifier drop-down menu in the Bar Button section of the Attributes Inspector.

You’ll use this button to cancel selecting a new destination.

image You don’t have to select a tint for the button in the Bar Button Tint section of the Attributes inspector. The app-wide tint color will be used for the button.

5.     Drag an Image view from the Library in the Utility area and place it in the Destination controller on the Canvas so that it takes up the entire view.

6.     Control-drag from the Image view in the Canvas or the Document Outline to the Top Layout Guide in the Document Outline and select Vertical Spacing.

7.     With the Image view selected, choose Editor⇒Resolve Auto Layout Issues⇒Add Missing Constraints.

If you have any warnings, use Editor⇒Resolve Auto Layout Issues subcommands to fix them. You may need to use Editor⇒Resolve Auto Layout Issues⇒Clear Constraints if you have to start over.

8.     In the Image View section of the Attributes inspector, select DestinationImage from the Image drop-down menu.

The appropriate image from the asset catalog will be used automatically. You downloaded those images in Chapter 3.

9.     Drag a Label from the Library and add it to the view toward the top of the Image view.

10.  With the Label selected, enter Pick a place in the Text field in the Label section of the Attributes inspector.

11.  Still in the Attributes inspector, change the style to Text Styles – Headline by selecting the Text icon in the Font field (which opens a window in which you can change the font size) as shown in Figure 20-2.

12.  Select the label and then choose Editor⇒Size Fit to Content from the Xcode main menu.

The label will expand to fit the text.

13.  Change the text color to white in the Text Color drop-down menu.

image

Figure 20-2: Set the text style.

14.  Position the Pick a place label, as shown in Figure 20-3.

A word of warning: You need to follow the next set of steps exactly. You can get the look you want in other ways, but this is the most straightforward. The Table view won't be transparent; you’ll fix that in viewDidLoad in the “Creating the Table View” section, later in the chapter.

15.  Drag a Table view (not Table View controller) from the Library onto the Image view and position it as shown in Figure 20-4.

This is the area in which the Table view will display. If you have more selections than can fit in the visible area, the user will be able to scroll the Table view.

16.  With the Table view selected, scroll down the Attributes inspector to reach the View section and then select Clear Color from the Background drop-down menu.

17.  Still in the Attributes inspector, scroll back up to the Table View section and choose Grouped from the Style menu.

18.  Enter 1 in the Prototype Cells field (or just use the stepper control to get to 1).

Leave these as prototype cells because you’ll provide the content for the cells programmatically.

image

Figure 20-3: Set the color.

image

Figure 20-4: Add a Table view.

19.  Select the prototype Table View Cell, either on the Canvas or in the Document Outline, and then choose Basic from the inspector’s Style menu and enter DestinationCell in the Identifier field.

You will need to have a reuse identifier (which I explain in the section “Displaying the cell,” later in this chapter).

20.  Still in the Attributes inspector, scroll down to the View section and select Clear Color from the Background drop-down menu.

21.  Select the Table View cell in the Document Outline, open the disclosure triangle, and select the label.

22.  In the Attributes inspector, scroll down and then choose Clear Color from the Background drop-down menu.

23.  Close the Utility area and select the Assistant in the Editor selector.

24.  If theDestinationController.h file doesn't appear, select it in the Jump bar.

25.  Control-drag from the Table view in Document Outline or on the Canvas to the DestinationController.h Interface. Release the mouse button, and in the dialog that pops up, enter outlet and destinationTableView.

26.  Control-drag from the Cancel button in Document Outline or in the Navigation bar on the Canvas to the DestinationController.h Interface. Release the mouse button, and in the dialog that pops up, select Action in the Connection drop-down menu and enter cancel in the Name field.

When all is said and done, you should see a screen that looks like Figure 20-5.

image

Figure 20-5: Ready to code.

Adding a Modal View

Most of the time, the user can control what is happening in the app. You provide the buttons and other interface elements, but the user chooses what to do and what interface elements to tap. Modal views interrupt that user control. They are presented on the screen and, although the user can tap within them, they remain front and center until the user dismisses them. They are used when you want the user to do something or resolve an issue before continuing to use the rest of the app. The device is not locked up because the user can use the Home button to move to another app, but as far as your app is concerned, it’s frozen until the modal view is dismissed.

The most common way to manage Modal views is by creating an Objective-C protocol that's adopted by the controller presenting the Modal view. The Modal view, when the user has selected an action or Cancel, sends a message to the presenting controller’s delegate method. The requesting controller then dismisses the Modal controller. Using this approach means that before it dismisses the Modal controller, the presenting controller can get any data it needs from it. That is the pattern that you will implement here.

You start implementing the Modal view by declaring the protocol and a few other properties you’ll need, as well as the protocols the DestinationController needs to adopt.

To get things started, add the bolded code in Listing 20-1 to Destination
­­Controller.h.

Listing 20-1: Updating the Destination Interface

  #import <UIKit/UIKit.h>
@protocol DestinationControllerDelegate;

@interface DestinationController : UIViewController   
              <UITableViewDelegate, UITableViewDataSource>

@property (weak, nonatomic) IBOutlet UITableView *destinationTableView;
@property (strong, nonatomic) id   
                 <DestinationControllerDelegate> delegate;
@property (nonatomic, readonly) NSUInteger selectedDestination;
- (IBAction)cancel:(id)sender;
@end

@protocol DestinationControllerDelegate
@required
- (void)destinationController:
     (DestinationController *)controller  
                             didFinishWithSave:(BOOL)save;
@end

The Objective-C language provides a way to formally declare a list of methods (including declared properties) as a protocol. You’ve used framework-supplied protocols extensively in this book, and now you're defining your own protocol.

You declare formal protocols with the @protocol directive. In Listing 20-1, you declared a DestinationControllerDelegate protocol with one method, destinationController:didFinishWithSave:, which is required. Required is the default; if you wanted to declare optional methods, you would use the keyword @optional, and all methods following that keyword would be optional. For example, consider this:

  @protocol SimpleDelegate
@optional
- (void)doNothing;
@end

You can have both @required and @optional methods in a protocol. It is common to group them together, but you can intersperse them if you want.

image If neither @required or @optional is specified, @required is assumed. However, it is better to be specific about what is required and what is optional. The @protocol DestinationControllerDelegate: statement (at the top) tells the compiler that a protocol is on the way. Like the@class statement, it says, “Trust me, you’ll find the protocol.” You need this here only because you added this:

  @property (strong, nonatomic) id  
                 <DestinationControllerDelegate> delegate;

This statement tells the compiler to type check whatever it is you assign to delegate to make sure that it implements the DestinationControllerDelegate protocol.

You also added the selectedDestination property, which you’ll use in the ViewController to determine which destination the user selected. Notice that you have made it read-only because there is no reason for any other object to be able to set it.

You also adopted two protocols from the Cocoa Touch framework, UITableViewDelegate and UITableViewDataSource, which you’ll use to manage the Table view.

Next, you’re going to need to update the DestinationController implementation in Listing 20-2 with the bolded code for some header files you’ll need to use later.

Listing 20-2: Updating the DestinationController Implementation

  #import "DestinationController.h"
#import "DetailViewController.h"
#import "AppDelegate.h"

@interface DestinationController ()
@end

@implementation DestinationController

Now that you have the plumbing in, you can look at what will go on in the DestinationController.

Implementing a Table View

The functionality in the DestinationController is in the Table view. You’ve worked with Table views before — but those used static cells, and all the work was done in the storyboard. Now it’s time to branch out on your own and understand what the storyboard was doing for you behind the scenes, as it were.

It’s a good thing to know how Table views work, because Table views are front and center in many apps that come with the iOS devices out of the box; they play a major role in many of the apps that you can download from the App Store. (Obvious examples: Almost all the views in the Settings, Mail, Music, and Contacts apps are Table views.) Table views take on such a significant role because, in addition to displaying data, they can also serve as a way to navigate structured data.

If you take a look at an app such as Mail or Settings, you find that Table views present a scrollable list of items (or rows or entries — I use all three terms interchangeably) that may be divided into sections. A row can display text or images. It may have an accessory such as a disclosure triangle, so that when you select a row, you may be presented with another Table view or with some other view that may display a web page or even controls such as buttons and Text fields. (You can see an illustration of this diversity back in Chapter 4, where Figure 4-6 shows how selecting Map leads to a Map view displaying a map of San Francisco, which is very handy when you roll into town.)

image It’s worth noting that iOS Table views only provide a single column of data — not the two-dimensional tables that you might build in a Numbers spreadsheet. The OS X Cocoa framework does provide a multi-column NSTableView class, but the IOS UITableView only supports a single column.

To kick off the Table view creation process, you first need to decide what you want to have happen when the user selects a particular row in the Table view of your app. As you saw with static cells, you can have virtually anything happen. You can display a Web view as you do in Weather or even display another Table view.

In this case, however, the Destination View controller will be dismissed, and the user will find herself in the master view, ready to make another selection.

image A Table view is an instance of the class UITableView, where each visible row of the table uses a UITableViewCell to draw its contents. Think of a Table view as the object that creates and manages the table structure, and the Table View cell as being responsible for displaying the content of a single cell of the table.

Creating the Table View

Although powerful, Table views are surprisingly easy to work with. To create a Table view, you follow only four — count ’em, four — steps, in the following order:

1.     Create and format the view itself.

This includes specifying the Table style and a few other parameters, most of which you do in Interface Builder.

2.     Specify the Table view configuration.

Not too complicated, actually. You let UITableView know how many sections you want, how many rows you want in each section, and what you want to call your section headers. You do that with the help of the numberOfSectionsInTableView:, tableView:numberOfRowsInSection:, andtableView:titleForHeaderInSection: methods, respectively.

3.     Supply the text (or graphic) for each row.

You return that from the implementation of the tableView:cellForRowAtIndexPath: method. This message is sent for each visible row in the Table view, and you return a Table View cell to display the text or graphic.

4.     Respond to a user selection of the row.

You use the tableView:didSelectRowAtIndexPath: method to take care of this task. In this method, you can create a view controller and push it onto the stack (as the storyboard does in a segue), or you can even send a message to the controller that presented a Modal View controller (or any other object).

image A UITableView object must have a data source and a delegate:

·        The data source supplies the content for the Table view.

·        The delegate manages the appearance and behavior of the Table view.

The data source adopts the UITableViewDataSource protocol, and the delegate adopts the UITableViewDelegate protocol — no surprises there. Of the preceding methods, only tableView:didSelectRowAtIndexPath: is included in the UITableViewDelegate protocol. All the other methods that I list earlier are included in the UITableViewDataSource protocol.

The data source and the delegate are often (but not necessarily) implemented in the same object, which is often a subclass of UITableViewController. UITableViewController adopts the necessary protocols and even furnishes some method stubs for you. In this case, the Table view is just another object in the DestinationController view. I had you do that when creating DestinationController earlier in the chapter so I could explain the real guts of Table views and because I wanted you to be able to display that Pick a Place label.

image There's another way to display a label such as Pick a Place using a UITableViewController. UITableView has a tableHeaderView property which is a view. You could create a view with the label, one or more images, and maybe another label and then assign that view totableHeaderView in a UITableView either standing alone as is the case here or situated within a UITableViewController.

Implementing these five (count ’em, five) methods (in the four steps earlier) is all you need to do to implement a Table view.

Not bad.

I already had you adopt the Table View delegate and Data Source protocols in Listing 20-1, so you are already partway there.

Add the bolded code in Listing 20-3 to the DestinationController.m file’s viewDidLoad method.

Listing 20-3: Updating viewDidLoad

  - (void)viewDidLoad
{
  [super viewDidLoad];
  self.destinationTableView.delegate = self;
  self.destinationTableView.dataSource = self;
}

As you might surmise, this makes the DestinationController both the delegate and the data source.

Adding sections

In a grouped Table view, each group is referred to as a section.

The two methods you need to implement to start things off are as follows:

  numberOfSectionsInTableView:(UITableView *)tableView

tableView:(UITableView *)tableView 
                 numberOfRowsInSection:(NSInteger)section

Each of these methods returns an integer, and that integer tells the Table view something — the number of sections and the number of rows in a given section, respectively.

Add the methods in Listing 20-4 to DestinationController.m to create a Table view that has one section with the number of rows equal to the number of destinations you have in your Destinations.plist. You will get compiler errors that you will fix with code in the next Listing.

Listing 20-4: Implementing numberOfSectionsInTableView: and tableView:numberOfRowsInSection:

  - (NSInteger)numberOfSectionsInTableView:
                                (UITableView *)tableView {
  return 1;
}
- (NSInteger)tableView:(UITableView *)tableView  
                numberOfRowsInSection:(NSInteger)section {
  NSString *filePath = [[NSBundle mainBundle] 
         pathForResource:@"Destinations" ofType:@"plist"];  
NSDictionary *destinations = 
    [NSDictionary dictionaryWithContentsOfFile: filePath];
  self.destinationsArray = destinations[@"DestinationData"];
  return [destinationsArray count];
}

The numberOfSectionsInTableView: method is obvious. In the tableView:numberOfRowsInSection: method, you do what you did in both the Trip and Events classes — you access Destination.plist to extract what you need. In this case, it’s the DestinationData array, which, to refresh your memory, is an array of dictionaries that have the data for each destination and return the count.

image Keep in mind that the first section is zero, as is the first row. This means, of course, that whenever you want to use an index to get to the first row or section, you need to use 0, not 1 — and an index of 1 for the second row and so on.

You’ll get an Xcode Live Issue error here because you need to add the new destinationsArray property (you’ll use this same array later in tableView:cellForRowAtIndexPath:). In addition, remember that you declared the selectedDestination property in DestinationController.h asreadonly. That is fine for the public interface, but you need to be able to set it from within DestinationController.m. You can do that by overriding the public property. (This is a very common pattern for a property — readonly to the public but readwrite within the implementation of the class that declares it.)

To do those things, add the bolded code in Listing 20-5 to DestinationController.m.

Listing 20-5: Updating the DestinationController Implementation

  #import "DestinationController.h"

@interface DestinationController () 
  @property (strong, nonatomic) 
    NSArray *destinationsArray;
  @property (nonatomic, readwrite) 
    NSUInteger selectedDestination;

@end

Displaying the cell

To display the cell content, your delegate is sent the tableView:cellForRowAtIndexPath: message. Add this method in Listing 20-6 to DestinationController.m.

Listing 20-6: Implementing tableView:cellForRowAtIndexPath:

  - (UITableViewCell *)tableView:(UITableView *)tableView 
          cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  static NSString *CellIdentifier = @"DestinationCell";
  UITableViewCell *cell = [tableView 
      dequeueReusableCellWithIdentifier:CellIdentifier];
  NSDictionary * destinationData = self.destinationsArray [indexPath.row];
  
  NSAttributedString *attributedString = [[NSAttributedString alloc]
           initWithString:destinationData[@"DestinationName"]
                           
          attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:17.0f],

           NSForegroundColorAttributeName:  [UIColor whiteColor]}];
  cell.textLabel.attributedText = attributedString;  
  return cell;
}

Walking through Listing 20-6, you see that one of the first things you do is determine whether any cells that you can use are lying around. You may remember that although a Table view can display quite a few rows at a time on the iPad’s screen, the table itself can conceivably hold a lot more. A large table can eat up a lot of memory, however, if you create cells for every row. Fortunately, Table views are designed to reuse cells. As a Table view’s cells scroll off the screen, they’re placed in a queue of cells available to be reused.

image If the system runs low on memory, the Table view gets rid of the cells in the queue, but as long as it has some available memory for them, it holds on to them in case you want to use them again.

You create a string to use as a cell identifier to indicate what cell type you’re using:

  static NSString *CellIdentifier = @"DestinationCell";

You recall that this is what you entered in the Identifier field of the Prototype cell in Step 18 in the “Setting up the DestinationController in the MainStoryboard_iPad” section, earlier in this chapter.

image It is critical that the CellIdentifier and the Identifier field of the Prototype cell in Step 18 are the same. If they are not, you won’t get the transparent prototype cell you specified in the storyboard.

image Table views support multiple cell types, which makes the identifier necessary. In this case, you need only one cell type, but sometimes you may want more than one to accommodate cells with different layouts and formats. For example, if only some cells should have a disclosure triangle, you would probably use two prototypes — one with and one without the disclosure triangle.

You ask the Table view for a specific reusable cell object by sending it a dequeueReusableCellWithIdentifier: message:

  UITableViewCell *cell = [tableView 
        dequeueReusableCellWithIdentifier:CellIdentifier];

This determines whether any cells of the type you want are available. If no cells are lying around, this method will create a cell using the cell identifier that you specified. You now have a Table View cell that you can return to the Table view.

You have several choices on how to format the Table View cell. Although you’re going to be using UITableViewCellStyleDefault, you can choose from a number of different styles, listed as follows (the keywords in the Style pop-up menu in the Attributes tab of Interface Builder are shown in brackets):

·        UITableViewCellStyleDefault: Gives you a simple cell with a Text label (black and left-aligned) and an optional Image view. [Basic]

·        UITableViewCellStyleValue1: Gives you a cell with a left-aligned black Text label on the left side of the cell and a right-aligned Text label with smaller gray text on the right side. (The Settings app uses this style of cell.) [Right Detail]

·        UITableViewCellStyleValue2: Gives you a cell with a right-aligned blue Text label on the left side of the cell and a left-aligned black Text label on the right side of the cell. [Left Detail]

·        UITableViewCellStyleSubtitle: Gives you a cell with a left-aligned Text label across the top and a left-aligned Text label below it in smaller gray text. (The Music app uses cells in this style.) [Subtitle]

With the formatting out of the way, you then set the Label properties that you’re interested in.

You pluck out the name for each destination you’ve stored by accessing the DestinationName in each Destination dictionary. You do that by accessing the dictionary in the (saved) destinationsArray corresponding to the sections and row in indexPath, which contains the section and row information in a single object. To get the row or the section out of an NSIndexPath, you just have to invoke its section method (indexPath.section) or its row method (indexPath.row), either of which returns an int:

  NSDictionary * destinationData = 
          destinationsArray[indexPath.row];

Next, create an attributed string, which can manage both the character strings and attributes such as fonts, colors, and even kerning:

  NSAttributedString *attributedString = [[NSAttributedString alloc]

           initWithString:destinationData[@"DestinationName"]

           attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:17.0f],

           NSForegroundColorAttributeName:  [UIColor whiteColor]}];

Now, use this attributed string to format the cell’s text label:

  cell.textLabel.attributedText = attributedString;

Finally, return the formatted cell with the text it needs to display in that row:

  return cell;

Working with user selections

Now you can look at what happens when the user selects a row with a destination displayed.

When the user taps a Table View entry, what happens next depends on what you want your Table view to do for you.

If you’re using the Table view to display data (as the Albums view in the Music app does, for example), you want a user’s tap to show the next level in the hierarchy, such as a list of songs or a detail view of an item (such as information about a song).

In the case of the RoadTrip app, you want a user’s tap to take you back to the Master view and, behind the scenes, create the correct model so that when you tap the Travel button, the right data is there.

To do that, add the final delegate method you need to implement, tableView:
didSelectRowAtIndexPath:. Add the code in Listing 20-7 to Destination
Controller.m.

Listing 20-7: Implementing tableView:didSelectRowAtIndexPath:

  - (void)tableView:(UITableView *)tableView 
         didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
  [tableView deselectRowAtIndexPath:
                                  indexPath animated:YES];
  self.selectedDestination = indexPath.row;
  [self.delegate destinationController:self didFinishWithSave:YES];
}

You set the selectedDestination property to the selected row. Then you send the delegate the destinationController:didFinishWithSave: message with a value of YES.

Before I explain the destinationController:didFinishWithSave: method, implement the last part of the DestinationController. Add the bolded code in Listing 20-8 to the cancel method (generated when you created the action) in DestinationController.m.

Listing 20-8: Adding cancel:

  - (IBAction)cancel:(id)sender {
  self.delegate destinationController:self 
                                    didFinishWithSave:NO];
}

When the user taps Cancel, the DestinationController sends the destinationController:didFinishWithSave: message with a value of NO to its delegate — which will be the MasterViewController. Now you’ll go back to the MasterViewController and implement thedestinationController:didFinishWithSave: message.

You also need to have the MasterViewController adopt the DestinationControllerDelegate protocol and declare the destinationController:
didFinishWithSave: method. To do that, add the bolded code in Listing 20-9 to MasterViewController.h.

Listing 20-9: Updating the MasterViewController Interface

  #import <UIKit/UIKit.h>
#import "DestinationController.h"
@class DetailViewController;

@interface RTMasterViewController : UITableViewController
     <UITextFieldDelegate, DestinationControllerDelegate>

@property (strong, nonatomic) RTDetailViewController *detailViewController;
@property (weak, nonatomic) IBOutlet UITextField *findText;
- (void)destinationController:(DestinationController *)
                  controller didFinishWithSave:(BOOL)save;

Next, add the destinationController:didFinishWithSave: method in Listing 20-10 to MasterViewController.m.

Listing 20-10: Adding destinationController:didFinishWithSave:

  - (void)destinationController:(DestinationController *)
                controller didFinishWithSave:(BOOL)save {
  
  AppDelegate *appDelegate = 
             [[UIApplication sharedApplication] delegate];
  
  if (save) {
    [appDelegate createDestinationModel:
                          controller.selectedDestination];
    [self viewDidLoad];
    DetailViewController* currentDetailViewController;  
    if ([[self.splitViewController.viewControllers  
                                             lastObject] 
         isKindOfClass:[UINavigationController class]]) {
      UINavigationController *navigationController = [self.splitViewController.viewControllers 
                                             lastObject];
    currentDetailViewController = (DetailViewController *)
                  navigationController.topViewController;
    }
    else
      currentDetailViewController = 
[self.splitViewController.viewControllers 
                                             lastObject];
    [currentDetailViewController viewDidLoad];
    
    if (currentDetailViewController.popOverButton) {
      if (![[self.splitViewController.viewControllers 
                                               lastObject] 
          isKindOfClass:[UINavigationController class]]) {
        NSMutableArray *itemsArray = [currentDetailViewController.toolbar.items 
                                             mutableCopy]; 
        [itemsArray removeObjectAtIndex:0]; 
        [currentDetailViewController.toolbar 
                         setItems:itemsArray animated:NO];
      }
    }
    if ([currentDetailViewController isKindOfClass:[MapController class]]) {
      NSMutableArray *itemsArray = 
        [currentDetailViewController.toolbar.items 
                                             mutableCopy]; 
      [itemsArray removeLastObject]; 
      [currentDetailViewController.toolbar 
                         setItems:itemsArray animated:NO];
    }
  }
  if (appDelegate.trip == nil) 
    [appDelegate createDestinationModel:0];
  [self dismissViewControllerAnimated:YES completion:nil];
}

If the user has chosen a new destination, you send the app delegate a message to create that model:

  [appDelegate 
   createDestinationModel:controller.selectedDestination];

It determines the selection the user made by accessing the selectedDestination property you set in the tableView:didSelectRowAtIndexPath: method.

As you may recall, createDestinationModel: is an already existing method in the app delegate. The createDestinationModel: method will actually be creating the model, and I made this a separate method because you’ll have to be able to send the AppDelegate a message to create a newTrip when the user chooses a new destination in Chapter 20. Well, here it is, Chapter 20, and that’s exactly what you’re doing.

You reload the Master view based on the new destination so you can change the Background image.

  [self viewDidLoad];

You’ll also need to update the Detail view.

  DetailViewController* currentDetailViewController;  
if ([[self.splitViewController.viewControllers lastObject] 
          isKindOfClass:[UINavigationController class]]) {
  UINavigationController *navigationController = 
    [self.splitViewController.viewControllers lastObject];
  currentDetailViewController = (DetailViewController *) 
                   navigationController.topViewController;
}  
else
  currentDetailViewController = 
    [self.splitViewController.viewControllers lastObject];
[currentDetailViewController viewDidLoad];
    
}

You’ll need to determine whether the current Detail view is embedded in a Navigation controller. Then you get the current Detail view and simply send it the viewDidLoad message, which will cause it to reload all its data.

You also need to take a nuance here into account. If the current view controller isn't embedded in a Navigation controller, that means it has a toolbar. If it already has a Road Trip button, to keep things in sync, you’ll need to remove the Road Trip button, which will then be added back in when the view reloads.

      
if (currentDetailViewController.popOverButton) {
  if (![[self.splitViewController.viewControllers 
       lastObject] isKindOfClass:
                        [UINavigationController class]]) {
    NSMutableArray *itemsArray = 
      [currentDetailViewController.toolbar.items 
                                             mutableCopy]; 
    [itemsArray removeObjectAtIndex:0]; 
    [currentDetailViewController.toolbar 
                         setItems:itemsArray animated:NO]; 
  }
}

And yet another thing: If the current Detail view is a Map view, you’ll also need to remove the Locate button if it’s on the toolbar:

  if ([currentDetailViewController isKindOfClass:[MapController class]]) {
  NSMutableArray *itemsArray = 
     [currentDetailViewController.toolbar.items 
                                             mutableCopy]; 
  [itemsArray removeLastObject]; 
  [currentDetailViewController.toolbar 
                         setItems:itemsArray animated:NO];
    }

If the user hasn’t chosen a new destination but no model exists yet (when the user first launches the program, for example, no model exists yet — you’ll see how that works in a second), you’ll have the app delegate create a model using a default destination. I have arbitrarily chosen the first one.

You then send the dismissModalViewControllerAnimated: message, which, as you might expect, dismisses the view controller using the transition you specified in the “Setting up the DestinationController in the MainStoryboard_iPad” section, earlier in this chapter.

If the user has canceled, you simply send the dismissViewControllerAnimated: completion: message, and the user finds herself back in the Master view.

But you still have some more work to do.

Previously, you added a delegate property to the Destination
Controller, which it uses when it sends the destinationController:
didFinishWithSave: message when the user selects a cell or taps Cancel.

The problem is, how do you set that property? Because you use a segue to take care of creating and initializing the controller, how do you assign the delegate property? If you recall from Chapter 19, when setting up FindController, you didn’t use a segue, so you could assign any property you wanted after you created (but before you added) the FindController to the Split View controller viewControllers in textFieldShouldReturn: (I’ve bolded where you do that in the MasterViewController.m code).

  - (BOOL)textFieldShouldReturn:(UITextField *)textField {
  
  [textField resignFirstResponder];
  
  if ([[UIDevice currentDevice] userInterfaceIdiom] == 
                                 UIUserInterfaceIdiomPad){
    FindController * findController = 
    [[UIStoryboard storyboardWithName:@"Main_iPad" 
                                            bundle:nil] 
        instantiateViewControllerWithIdentifier:@"Find"];
   findController.findLocation = textField.text;
...
)

Fortunately, you have a way to use a segue and still be able to pass some data on to the view controller that’s being instituted by the segue.

prepareForSegue:sender: is a view controller method used to notify the view controller that a segue is about to be performed. segue is the UIStoryboard
Segue object that contains information about the view controllers involved in the segue. You’ve already used prepareForSegue:sender:to dismiss the popover and assign the popOverButton and masterPopoverController properties. Now you need to add the code in bold in Listing 20-11 to prepare
ForSegue:sender: in MasterViewController.m. (I’ve omitted the code that was already there.)

Listing 20-11: Update prepareForSegue:sender:

  - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { 
      
  if ([segue.identifier isEqualToString:@"Destination"]) {

     if ([[UIDevice currentDevice] userInterfaceIdiom] == 
                               UIUserInterfaceIdiomPad) {
      DetailViewController *currentDetailViewController;

       DestinationController *destinationController = (DestinationController *)
            segue.destinationViewController;
      destinationController.delegate = self;

     if ([[self.splitViewController.viewControllers 
                                               lastObject] 
          isKindOfClass:[UINavigationController class]]) {       
      UINavigationController *navigationController = [self.splitViewController.viewControllers 
                                              lastObject];
      currentDetailViewController =
         (DetailViewController *)       
                   navigationController.topViewController;
    }
    else
      currentDetailViewController = [self.splitViewController.viewControllers 
                                             lastObject];
    if (currentDetailViewController.
                           masterPopoverController != nil) 

      [currentDetailViewController.
                     masterPopoverController            
                             dismissPopoverAnimated:YES];
    }  
    else {
      DestinationController *destinationController = (DestinationController *)   
                         segue.destinationViewController;
      destinationController.delegate = self;
    }
    return;
  }
... // previous code here
}

You first check to see whether the segue is the Destination segue (see, those identifiers are really useful):

  if ([destinationSegue.identifier  
                          isEqualToString:@"Destination"])

If it's the Destination segue, you check to see whether the device is an iPad. If it is, you go through the usual logic to find the Detail View (Destination) controller and assign its delegate to self.

  DetailViewController *currentDetailViewController;
      
DestinationController *destinationController = 
                                 (DestinationController *)
[segue.destinationViewController topViewController];
                    destinationController.delegate = self;

You then go through the usual logic and find the current Detail View controller and dismiss the popover, if one exists.

  if ([[self.splitViewController.viewControllers lastObject] 
          isKindOfClass:[UINavigationController class]]) {       
UINavigationController *navigationController =
    [self.splitViewController.viewControllers lastObject];
currentDetailViewController = (DetailViewController *)       
                   navigationController.topViewController;
}
else
  currentDetailViewController = 
    [self.splitViewController.viewControllers lastObject];
if (currentDetailViewController.
                           masterPopoverController != nil) 
       
  [currentDetailViewController.masterPopoverController            
                              dismissPopoverAnimated:YES];

If you’re on the iPhone, you simply assign the delegate to the segue’s destinationViewController.

  DestinationController *destinationController = 
               destinationSegue.destinationViewController;
destinationController.delegate = self;

Saving the Destination Choice and Selecting a Destination

At this point, if you were to run your project, you would be able to tap the Destination button, choose a destination, and see the data for either New York or San Francisco.

But you’re not done yet.

First, if the app is terminated (and I mean terminated, not running in the background and relaunched), the user will find that the destination she selected has reverted to being the default one. You would like RoadTrip to be in position to save, and then restore, the user’s destination preference. (In Chapter 11, you see how to default to the first destination in the plist. I mention in that chapter that I show you how to allow the user to select a destination in Chapter 20, and here you are.)

Start by adding the destinationPreference property to AppDelegate by adding the bolded code in Listing 20-12 to AppDelegate.h.

Listing 20-12: Updating the AppDelegate Interface

  #import <UIKit/UIKit.h>
@class Trip;
@interface AppDelegate : UIResponder 
                                 <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic, strong) Trip *trip;
@property (nonatomic, strong) NSString *  
                                    destinationPreference;
- (void) createDestinationModel:(int)destinationIndex;
@end

Apple provides an NSUserDefaults object — available to any app — that you can use to store user preferences, or any small data values that should be saved by your app. In this section, you use the NSUserDefaults object to save the destination preference, so if the user chooses “San Francisco” the first time they use the app, “San Francisco” will be the location used when the app is next launched.

Data is stored in the user defaults object as a key-value pair. The value will be the destinationPreference string (which will be the index of the destination — @“0” or @“1” at this point). The key will be the static Destination
PreferenceKey that you should now add to AppDelegate.m, as shown by the bolded code in Listing 20-13.

Listing 20-13: Updating the AppDelegate Implementation

  #import "AppDelegate.h"
#import "Reachability.h"
#import "Trip.h"

static NSString *DestinationPreferenceKey = 
                            @"DestinationPreferenceKey";
@implementation AppDelegate

You’re adding a key (string) that you’ll need to use when you save the preference.

What you would like to do is direct the user to select a destination rather than using the default one. I’ll have you post an alert to the users that they need to do that. More elegant ways are available to get the users to select the initial destination, but I’ll leave that as an exercise for the reader.

Start by adding the bolded code in Listing 20-14 to viewDidLoad in Master
ViewController.m.

Listing 20-14: Adding to viewDidLoad

  - (void)viewDidLoad
{
  [super viewDidLoad];
  AppDelegate* appDelegate = [[UIApplication sharedApplication] delegate];
  self.title = appDelegate.trip.destinationName;
  UIImageView* imageView = [[UIImageView alloc] 
      initWithImage:[appDelegate.trip destinationImage]];
  self.tableView.backgroundView = imageView;
  
  UISwipeGestureRecognizer *swipeGesture = 
    [[UISwipeGestureRecognizer alloc] initWithTarget:self 
                   action:@selector(handleSwipeGesture:)];
  swipeGesture.direction =  UISwipeGestureRecognizerDirectionLeft;
  [self.view addGestureRecognizer:swipeGesture];
  self.findText.delegate = self;
  
  if(appDelegate.destinationPreference == nil) { 
    UIAlertView *alert = [[UIAlertView alloc] 
         initWithTitle:@"Welcome to Road Trip"
         message:@"Please select a Destination from the 
                                          Road Trip Menu" 
         delegate:nil
         cancelButtonTitle:@"OK" 
         otherButtonTitles:nil];
    [alert show];
  }
}

As you can see, if the appDelegate.destinationPreference property is nil, you’ll post the Please select a destination from the Road Trip menu alert.

Unfortunately, every time you compile and run your app (or launch it), you’ll see the alert because you will never have anything other than nil in the appDelegate.destinationPreference property.

In Listing 20-15, you fix that problem. At application launch, you’ll check to see whether a user preference is saved. If one is, you assign it to destinationPreference. If no preference is saved, you leave that preference as nil, and the alert to the user to select a destination will be posted by the MasterViewController.

Add the bolded code in Listing 20-15 to application:didFinishLaunching
WithOptions: in AppDelegate.m and delete the one line of code that's commented out in bold, italic, and underline.

Listing 20-15: Updating application:didFinishLaunchingWithOptions:

  - (BOOL)application:(UIApplication *)application 
    didFinishLaunchingWithOptions:
                             (NSDictionary *)launchOptions
{
  ... // previous code here
  self.destinationPreference = [[NSUserDefaults standardUserDefaults] objectForKey:DestinationPreferenceKey];
  if (self.destinationPreference == nil) {
    NSDictionary *currentDestinationDict = @{DestinationPreferenceKey: @"0"};
    [[NSUserDefaults standardUserDefaults]     
                registerDefaults:currentDestinationDict];
  }
  else
    [self createDestinationModel:
                   [self.destinationPreference intValue]];
//[self createDestinationModel:0];>

  return YES;
}

At app launch, you check an NSUserDefaults object to see whether an entry exists with a key of DestinationPreferenceKey (you added this previously in Listing 20-13):

  self.destinationPreference = 
    [[NSUserDefaults standardUserDefaults]             
                  objectForKey:DestinationPreferenceKey];

You use NSUserDefaults to read and store preference data to a defaults database, using a key value, just as you access keyed data from an NSDictionary. In this case, the preference is the destination.

NSUserDefaults is implemented as a singleton, meaning that only one instance of NSUserDefaults is running in your app. To get access to that one instance, I invoke the class method standardUserDefaults:

  [NSUserDefaults standardUserDefaults]

standardUserDefaults returns the NSUserDefaults object. As soon as you have access to the standard user defaults, you can store data there and then get it back when you need it.

objectForKey: is an NSUserDefaults method that returns the object associated with the specified key, or nil if the key wasn't found.

image You can add your app's preferences to Settings and then retrieve the values in the same way you are doing here. That is appropriate for settings that you want the user to set directly as opposed to a setting such as the last destination viewed that is managed automatically. Obviously, the first time the app is launched, no data is there, so you create a dictionary with the default value:

  NSDictionary *currentDestinationDict = 
@{DestinationPreferenceKey: @"0"};

Note that you save the value as an NSString. That’s because the NSUserDefaults requires a property list object.

You then send the NSUserDefaults object the registerDefaults message. This creates a new entry in the NSUserDefaults database that you can later access and update using the key you provided in the dictionary.

Because destinationPreference is still nil, when viewDidLoad executes, it will launch the Destination controller.

If a value exists in NSUserDefaults, you create the Destination model by sending the createDestinationModel: message with the value you had stored — which will be, as you will see, the index of the destination in the Destinations plist:

  [self createDestinationModel:
                   [self.destinationPreference intValue]];

Note that you use an NSString method intValue. This method returns the value in a string as an int, which is handy because that's what the create
DestinationModel: method expects.

You also could've made the currentDestinationIndex an NSNumber. It's an object wrapper for any C scalar (numeric) type. It defines a set of methods that allow you to set and access the value in many different ways, including as a signed or unsignedint, double, float, BOOL, and others. Also, NSNumber defines a compare: method to determine the ordering of two NSNumber objects.

image Using the index number of the destination rather than the name is a common coding practice. You need to be able to quickly go to a specific destination in the array of destinations. Each one has a title and a subtitle for use in displays.

If no destinationPreference exists, the user will see a blank Detail view with a default Master view (it looks a little different on the iPhone) and the alert asking her to select a destination. As I said, you have more elegant ways of doing this.

The last step in saving the Destination preference is actually storing destinationPreference itself, and you do that in createDestinationModel:. Add the bolded code in Listing 20-16 to createDestinationModel: in AppDelegate.m.

Listing 20-16: Updating createDestinationModel:

  - (void) createDestinationModel:(int)destinationIndex {
  NSString *selectedDestinationIndex = 
      [NSString stringWithFormat: @"%i",destinationIndex];
  if(![selectedDestinationIndex
           isEqualToString:self.destinationPreference]) {
    self.destinationPreference = selectedDestinationIndex;
    [[NSUserDefaults standardUserDefaults] 
           setObject:self.destinationPreference  
           forKey:DestinationPreferenceKey];
  } 
self.trip = [[Trip alloc] initWithDestinationIndex:destinationIndex];
}

You start out in Listing 20-16 by converting the destinationIndex parameter to a string and comparing it to see whether the Destination preference is the same as the one just selected by the user. (The user may have chosen the same destination again in the Destination controller.)

  NSString *selectedDestinationIndex = 
      [NSString stringWithFormat: @"%i",destinationIndex];
if(![selectedDestinationIndex
           isEqualToString:self.destinationPreference]) {

If the destination isn't the same, you assign the new value to the destinationPreference:

  self.destinationPreference = selectedDestinationIndex;

and then you save the new value in NSUserDefaults:

   [[NSUserDefaults standardUserDefaults] 
    setObject:self.destinationPreference  
                         forKey:DestinationPreferenceKey];

To store data, you use the setObject:forKey: method. The first argument, setObject:, is the object I want NSUserDefaults to save. This object must be NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. In this case, savedData is an NSString, so you’re in good shape.

The second argument is forKey:. To get the data back (and for NSUserDefaults to know where to save it), you have to be able to identify it to NSUserDefaults. You can, after all, have a number of preferences stored in the NSUserDefaults database, and the key tells NSUserDefaults which one you’re interested in.

Next, you create the model passing in the destination index:

  self.trip = [[Trip alloc]     
              initWithDestinationIndex:destinationIndex];

Displaying the Destination table

One remaining problem is that the Destination table should appear automatically when the user dismisses the UIAlertView — the one that displays the “Welcome to Road Trip” message the first time the app is launched. The best way to handle this is to provide a method that will be called when the Alert is dismissed by the user, and then display the Destination table in that method. Here are the steps to do that:

1.     AddUIAlertViewDelegate to theMasterViewController’s comma-separated list of delegates inMasterViewController.h.

2.     Designate the MasterViewController as the UIAlertViewDelegate by adding the line of code shown in Listing 20-17 to the viewDidLoad method in MasterViewController.m.

3.     Add the alertView:clickedButtonAtIndex: method to MasterViewController.m. The simple code shown in Listing 20-18 displays the Destination table as desired.

Listing 20-17: Designating the Master View Controller as the Alert Delegate

  - (void) createDestinationModel:(int)destinationIndex {
... // previous code
if(appDelegate.destinationPreference == nil) { 
    UIAlertView *alert = [[UIAlertView alloc] 
         initWithTitle:@"Welcome to Road Trip"
         message:@"Please select a Destination from the 
                                          Road Trip Menu" 
         delegate:nil
         cancelButtonTitle:@"OK" 
         otherButtonTitles:nil];
    alert.delegate = self;
    [alert show];
  }

Listing 20-18: Displaying the Destination Table

  - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
[self performSegueWithIdentifier:@"Destination" sender:self];
}

Testing

You’re done. Run your app and test your work.

image To test this part of your app, you need to first stop it in the Simulator (or device) by clicking the Stop button on the Xcode toolbar. Then remove RoadTrip from the background by following these steps:

1.     Double-click the Home button Hardware⇒Home to display the apps running in the background.

2.     Drag the RoadTrip view up and out of the horizontal list of background apps. If it’s not there, it’s not running in the background.

3.     Run your app.

To test this part of your app again later, you need to first stop it in the Simulator (or device) by clicking the Stop button on the Xcode toolbar. Then remove RoadTrip and its user defaults from the device by following these steps:

1.     Press and hold the RoadTrip icon until it wiggles.

2.     Click the app Delete icon, the circle with the X that appears in the upper-left corner of the icon.

3.     Press the Delete button when asked if you should delete the app and all of its data.

4.     Build and run your app again, with the default destinationPreference again set to nil. This gives you a fresh start.

Adding Destination Support to the iPhone Storyboard

Your goal is to add a Destination scene to your iPhone storyboard in the same way that you added one to your iPad storyboard file. Follow the same directions for creating the Destination Controller scene for iPad storyboard. Steps include the following:

1.     Drag a UIViewController into the iPhone storyboard.

2.     Use the Inspector to change the class name toDestinationController as well as the Storyboard ID and Title to Destination.

3.     Drag a Modal segue from the Destination table cell in the MasterViewController to the DestinationController. Set the segue ID to Destination.

4.     Add the Navigation bar, and then place a Cancel button in it.

5.     Choose the DestinationController’s cancel action for the Cancel button.

6.     Add a UIImageView with an image.

7.     Add a Label “Pick a Place, Anyplace.”

8.     Add a Table view.

9.     Set the Table view’s delegate and dataSource to be the DestinationController.

10.  Connect the Table view to the destinationTableView property in Destination.m.

11.  Format the Table View cell as described earlier in the chapter.

The resulting layout is shown in Figure 20-6.

The good news is that you don’t have to change your Objective-C code at all — the same DestinationController code works fine.

image

Figure 20-6: The Destination scene in the iPhone storyboard.

A Word about Adding Settings

Although space doesn't allow me to show you how to implement settings — for example, letting the user choose whether she wants to hear the car sound when she taps the Test Drive button, or to change the speed of the car — you implement such settings in exactly the same way that you just implemented the Destination preference. You add a setting to NSUserDefaults and create an AppDelegate property that you check in the car animation messages, for example, before you play the sound. To get even more sophisticated, you could create a Preferences class, in the same way you create a Trip class, that manages all preferences and uses that rather than the AppDelegate to provide Preference data to the rest of your app.

What’s Next?

Although this point marks the end of your guided tour of iOS app development, it should also be the start — if you haven’t started already — of your own development work.

Developing for iOS is one of the most exciting opportunities I’ve come across in a long time. I’m hoping that it ends up being as exciting for you.

Do keep in touch, though. Check out my website, www.northcountryconsulting.com, on a regular basis. There you can find the completed RoadTrip Xcode project as well as any updates.

Finally, keep having fun. I hope I have the opportunity to download one of your apps from the App Store soon.