I like my coffee with two sugars, cream, a sprinkle of cinnamon, stirred twice, then ...
Most iPhone apps have more than one view.
We've written a cool app with one view, but anyone who's used an iPhone knows that most apps aren't like that. Some of the more impressive iPhone apps out there do a great job of moving through complex information by using multiple views. We're going to start with navigation controllers and table views, like the kind you see in your Mail and Contact apps. Only we're going to do it with a twist...
Look, I don't have time for posting to Twitter. I need to know a ton of drink recipes every night. Is there an app for that?
Sam, bartender at the HF Lounge
Before you pick the template for our bartending app, take a minute to look at how you want the user to interact with the drink information. We're going to have a scrollable list of drink names, and when the user taps on a row, we'll show the detailed drink information using view #2, our detailed view. Once our user has seen enough, they're going to want to go back to the drink list.
Title
Drink #1
Drink #1
Drink #1
Name:
Ingredients:
Directions:
Once our users are done with the detailed information, the Navigation bar gives them a way to get back to the list.
Name:
Ingredients:
Directions:
We're going to want some kind of transition between these views...
We need a list of items to work with...
Title
Name:
Ingredients:
Directions:
We're going to be coming in and out of this view a lot – each time our user selects a drink.
For this app, we're going to use a Navigation-based project. To get started, go into Xcode and choose the File→New Project option. Choose the Navigationbased application and save it as DrinkMixer.proj. Make sure that "Use Core Data for storage" is not checked.
The navigation template comes with a lot of functionality built in:
Just like the name says, a navigation controller is built in. It provides back buttons, title bars, and a view history that will keep your user moving through the data without getting lost.
Don't check Core Data. We'll use that later in the book.
Lemon Drop
Firecracker
We have hierarchical data to organize. The navigation template helps us to move through the data, starting with a table view.
Lemon Drop: Citron vodka, lemon, and sugar. Add sugar to the rim of glass, pour ingredients into shaker...
Firecracker: Wild turkey and hot sauce. Pour ingredients into a rocks glass filled with ice
.
The Navigation Controller provides transitions between views with animations.
The navigation template comes with a navigation controller and a root view that the controller displays on startup. That root view is set up as a table view by default, and that works great for our app. A table view is typically used for listing items, one of which then can be selected for more details about that item.
Navigation template
The navigation controller provides a navigation bar.
This is where you'll find the back buttons, forward buttons, and the title of the view you're in.
The table view
The table view provides an easy way to work with data. It starts with an empty, scrollable list for the main view of your application.
there are no: Dumb Questions
The UITableView provides a lot of the functionality we need right away, but it still needs to know what data we're actually trying to show and what to do when the user interacts with that data. This is where the datasource and delegate come in. A table view is easy to customize and is set up by the template to talk to the datasource and delegate to see what it needs to show, how many rows, what table cells to use, etc.
The navigation controller, not the table view, provides the navigation bar. Since we're in interface builder, this is just a simulated one.
Table views have built-in support for editing their contents, including moving rows around, deleting rows, and adding new ones.
Table views can tell you when your user taps on a cell. It'll tell you the section and row that was tapped.
We're using the default table view cell, but you can create your own and lay them out any way you want.
Table views try to conserve memory by reusing cells when they scroll off the screen.
A table can only have one column, but you can put whatever you want in that column by customizing your table cells.
A table can have multiple sections, and each section can have a header and a footer. We only have one section, so we don't need either for DrinkMixer.
A table view is made up of multiple table cells. The table view will ask how many cells (or rows) are in each section.
there are no: Dumb Questions
Drink List: Firecracker Lemon Drop Mojito
Wait, memory on the iPhone is a big deal, right? How can we put in all those drinks?
Like everything else on iPhone, the UITableView has to worry about memory.
So, how does it balance concerns about memory with an unknown amount of data to display? It breaks things up into cells.
The UITableView only has to display enough data to fill an iPhone screen—it doesn't really matter how much data you might have in total. The UITableView does this by reusing cells that scrolled off the screen.
The cells that are off the view go into a bucket until iPhone needs memory or the table view can reuse them when the user scrolls.
When the table view has to scroll a new row onto the screen, it asks the datasource for a cell for that row.
ecracker
Captain
As the user scrolls, some cells slide off the screen.
This is the active view with the table cells that are currently visible.
Firecracker
Lemon Drop
Absolut Mixer
Bee Stinger
Cupid's
Mojito
Miami Vice
Captain
The datasource checks the cell bucket to see if there are any cells available to reuse. If so, it just replaces the row's contents and returns the row.
Datasource
If there aren't any for reuse, the datasource creates a new one and sets its content.
The tableview takes the new cell and scrolls it in...
there are no: Dumb Question
Q: | You mentioned the table view's datasource and delegate, but why didn't I have to declare anything like we did with UIPickerView? |
A: | Great catch. Normally you would, but the navigation-based template we used already set this up. To see what's happening, look at the RootViewController.h file. You'll see that it is a subclass of UITableViewController, and that class conforms to the UITableViewDataSourceProtocol and the UITableViewDelegateProtocol. If you look in RootViewController.xib, you'll see that the table view's datasource and delegate are both set to be our RootViewController. If we weren't using a template, you'd have to set these up yourself (we'll revisit this in Chapter 7). |
Q: | I noticed we used an NSMutableArray. Is that because we had to initialize it? |
A: | No—both NSMutableArray and NSArray can be initialized with values when you create them. We're using an NSMutableArray because we're going to manipulate the contents of this array later. We'll get there in a minute. |
Q: | What's the nil at the end of the rink names when we create the drink array? |
A: | NSMutableArray's initializer takes a variable number of arguments. It uses nil to know it's reached the end of the arguments. The last element in the array will be the value before the nil—nil won't be added to the array. |
Q: | Tell me again about that @ symbol before our drink names? |
A: | The @ symbol is shorthand for creating an NSString. NSArrays store arrays of objects, so we need to convert our text names (char*s) to NSStrings. We do that by putting an @ in front of the text constant. |
Q: | When we customized the table view cells, we used the cell.textLabel. Are there other labels? What's the difference between cell.textLabel and cell.text? |
A: | Before iPhone 3.0, there was just one label and set of disclosure indicators in the default cell, and it was all handled by the cell itself. You just sent the text you wanted on the cell.text property. Nearly everyone wanted a little more information on the table cells, so in iPhone 3.0, Apple added a few different styles with different label layouts. Once they did that, they introduced specific properties for the different text areas, like textLabel, detailLabel, etc., and deprecated the old cell.text property. You shouldn't use cell.text in your apps—Apple will likely remove it at some point in the future. We'll talk more about the other labels later in the chapter. |
Q: | You mention that we can use section headers and footers—how do you specify those? |
A: | The datasource is responsible for that information, too. There are optional methods you can provide that return the title for section headers and the title for section footers based on the section number. They work a lot like our cellForRowAtIndexPath, except they only return strings. |
Q: | What's the difference between a plain table view and a grouped table view? |
A: | The only difference is the appearance. In a plain table view, like the one we're using, all the sections touch each other and are separated by the section header and footer if you have them. In a grouped table view, the table view puts space between the sections and shows the section header in bigger letters. Take a look at your contact list, then select a contact. The first view, where all of your contacts are listed together and separated by letters is a plain table view. The detailed view, where the phone numbers are separated from email addresses, etc, is a grouped table view. |
The drink menu at Head First Lounge has 40 cocktails.
Firecracker
Lemon Drop
Mojito
Absolut Mixer
Bee Stinger
Cupid's Cocktail
Strawberry Daquiri
Long Island Ice Tea
Captain and Coke
Miami Vice
Boxcar
Cat's Meow
Apple Martini
Manhattan
After Dinner Mint
Red Rudolph
Day at the Beach
Melon Tree
Rum Runner
Blue Dog
Key West Lemonade
Neapolitan
Polo Cocktail
Purple Yummy
Neon Geek
Flaming Nerd
Letter Bomb
Bookmaker's Luck
Baked Apple
Deer Hunter
Mexican Bomb
Aftershock
Black Eyed Susan
Beetle Juice
Terminator
Gingerbread Man
Lost in Space
Music City Sunset
Cafe Joy
Sandbar Sleeper
Get ready to start typing...
This sucks. Can't we just import the list Sam sent us somehow?
We could, but not the way we're set up now.
Since the drinks are populated with an array that's hardcoded into the implementation file, we can't import anything.
What would work well is a standardized way to read and import data; then we would be able to quickly get that drink list loaded.
Plist stands for "property list" and it has been around for quite a while with OS X. In fact, there are a number of plists already in use in your application. We've already worked with the most important plist, DrinkMixer-Info.plist. This is created by Xcode when you first create your project, and besides the app icons, it stores things like the main nib file to load when the application starts, the application version, and more. Xcode can create and edit these plists like any other file. Click on DrinkMixer-Info.plist to take a look at what's inside
Some of these items are obvious, like the icon file and the main nib to load.
Others are less obvious, but we'll talk more about them in later chapters.
Built-in types can save and load from plists automatically
All of the built-in types we've been using, like NSArray and NSString, can be loaded or saved from plists automatically. We can take advantage of this and move our drink list out of our source code.
- (void)viewDidLoad { [super viewDidLoad];NSMutableArray* tmpArray = [[NSMutableArray alloc]
initWithObjects:@"Firecracker", @"Lemon Drop", @"Mojito",nil];
self.drinks = tmpArray;
[tmpArray release];
// Uncomment the following line to display an Edit button in the navigation bar for this view controller. // self.navigationItem.rightBarButtonItem = self. editButtonItem; }
We'll move our drink list out of the source code here and into a plist instead...
Changing the array initialization code to use the plist is remarkably easy. Most Cocoa collection types like NSArray ad NSDictionary have built-in support for serializing to and from a plist. As long as you're using built-in types (like other collections, NSStrings, etc.,) you can just ask an array to initialize itself from a plist.
The only piece missing is telling the array which plist to use. To do that, we'll use the project's resource bundle, which acts as a handle to applicationspecific information and files.
-(void)viewDidLoad { [super viewDidLoad];NSString *path = [[NSBundle mainBundle] pathForResource:
@"DrinkArray" ofType:@"plist"];
NSMutableArray *tmpArray =[[NSMutableArray alloc]
initWithContentsOfFile:path]
; self.drinks = tmpArray; [tmpArray release]; ...
Ask the app bundle for a path to our DrinkArray plist.
Initialize the array using the contents of the plist.
RootViewController.m
Now we just need to get that detail view all set up, right?
Creating your detail view will complete the app.
The entire list of drinks is great, but Sam still needs to know what goes in them and how to make them. That information is going to go in the detail view that we sketched up earlier.
Earlier, we classified DrinkMixer as a productivity app and we chose a navigation controller because we have hierarchical data. We have a great big list of drinks loaded, but what Sam needs now is the detailed information for each drink: what are the ingredients, how do you mix them, etc. Now we'll use that navigation controller to display a more detailed view of a drink from the list.
The standard pattern for table views is that you show more information about an item when a user taps on a table cell. We'll use that to let the user select a drink then show our detailed view. The detail view follows the same pattern as our other views:
When the user taps on a drink, we'll display the detail view.
Touch here.
View Controller
The table view's controller (our RootViewController) will get the touch information. It will tell the nav controller to show the detailed view.
The detail view shows all the elements that make up a drink - the ingredients and how to mix them.
Detail
View
Since the detail view only cares about the specific drink it's showing details for, the datasource will focus on one drink.
Datasource
View Controller
Just like our other views, the detail view will have a view controller. This one will be responsible for filling in the detail view.
We sketched out the detail view earlier—but we need to look more closely at what we're about to build.
The back button comes with the nav controller
It will be populated with "Name:" and the drink info, so we don't need a label
A couple of labels for the bottom two fields
Back button
UITextField for the drink name
UITextView for the ingredients
UITextView for the directions
Let's start building...
there are no: Dumb Questions
OK, so I have an order for a Melon Tree... but I still don't see the drink details.
Touch here
We still need to get that detail view to load when Sam selects a drink.
Now that we've got the table view populated and the detail view built, it's time to manage moving between the two views. The navigationbased template comes preloaded with the functionality we need:
A view stack for moving between views
As users move back and forth, you can ask the navigation controller to display the appropriate view. The navigation controller keeps track of where the users are and gives them buttons to go back.
A navigation bar for buttons and a title
The navigation controller interacts with the navigation bar to display buttons that interact with the view being shown, along with a title to help the users know where they are.
A navigation toolbar for view-specific buttons
The navigation controller can display a toolbar at the bottom of the screen that shows custom buttons for its current view.
The UINavigationController supports a delegate, called the UINavigationControllerDelegate, that gets told when the controller is about to switch views, but for DrinkMixer we won't need this information. Since the views get told when they're shown and hidden, that's all we need for our app.
Now we need to get the table view and nav controller working together to display the detail view.
We've been dragging the navigation controller along since the beginning of this project, and now we finally get to put it to use. The navigation controller maintains a stack of views and displays the one on top. It will also automatically provide a back button, as well as the cool slide-in and out animations. We're going to talk more about the whole navigation controller stack in the next chapter, but for now, we're just going to push our new view onto the stack and let the controller take care of the rest. We just need to figure out how to get that new view.
When a row is tapped, tableview:did SelectRowAtIndexPath: indexPath is sent to the delegate.
Add Drink View
Drink Table View
Once the new view is created, we'll use the navigation controller to push the view onto the screen.
Delegate
When the delegate method is called, our RootViewController (the delegate) needs to create and push the detail view controller.
When a table row is touched, the table view calls tableview
:didSelectRowA tIndexPath
: on its delegate. The table passes along an NSIndexPath
(just like cellForRowAtIndexPath
) that tells us which row was selected.
Here's where it gets interesting: our RootViewController is our delegate, so it needs to hand off control to the view controller for our detail view...
The only piece left to create is the view controller. Instantiating a view controller is no different than instantiating any other class, with the exception that you can pass in the nib file it should load its view from:
[[DrinkDetail
ViewController alloc] initWithNibName:@"DrinkDetailView Controller"
bundle:nil];
Once we've created the the detail view controller, we'll ask the NavigationController to push the new view controller onto the view stack. Let's put all of this together by creating the callback into the delegate and creating the new view controller to push onto the stack:
#import "RootViewController.h"
#import "DrinkDetailViewController.h"
Since we're going to create the new view controller, we need to include its header.
// Override to support row selection in the table view. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // Navigation logic may go here -- for example, create and push another view controller.DrinkDetail
ViewController*drinkDetail
ViewController = [[DrinkDetail
ViewController alloc] initWithNibName:@"DrinkDetailViewController
" bundle:nil]; [self.navigationController pushViewController:drinkDetailViewController
animated:YES]; [drinkDetailViewController
release]; }
Here's the delegate callback - the indexPath tells us which row (drink) was selected.
Instantiate the controller...
...then push it onto the navigation stack.
Now that the navigation controller has the detail controller, we can release our reference to it.
RootViewController.m
Let's try this out...
So, now we can get to the detail view from the drink list, but there aren't any details in there. We don't have that info in our plist, do we?
We've outgrown our array
All that's left is to get the ingredients and directions in the detail view, and we'll have a bartender's brain. To save you from having to type in the ingredients and directions, we put together a new file with all of the extra information. The problem is we can't just jam that information into an array. To add the drink details to this version, we need a different data model.
Our current drink plist is just a single array of drink names. That worked great for populating the table view with just drink names, but doesn't help us at all with drink details. For this plist, instead of an array of strings, we created an array of dictionaries. Within each dictionary are three keys: name
, ingredients
, and directions
. Each of these have string values with the corresponding information. Since NSDictionary adopts the NSCoding protocol, it can be saved and loaded in plists just like our basic array from before.
there are no: Dumb Questions
Something has gone wrong, but honestly, this is a pretty normal part of the development process. There are lots of things that could cause our application to crash, so we need to figure out what the problem is.
In general, if your application doesn't build, Xcode won't launch it—but that's not true for warnings. Xcode will happily compile and run an application with warnings and your only indication will be a little yellow yield sign in the bottom right corner of Xcode. Two minutes spent investigating a warning can save hours of debugging time later.
3 errors and 3 warnings... the errors have to be fixed. The warnings should be investigated—and probably fixed, too.
That's not our problem, though: our code should be warning and compile-error-free. The good news is that when an app crashes in the Simulator, it doesn't go away completely (like it would on a real device). Xcode stops the app right before the OS would normally shut it down. Let's use that to see what's going on.
Time for some debugging...
We need to figure out why our app crashed, and thankfully, Xcode has a lot of strong debugging capabilities. For now we're just going to look at the information it gave us about the crash, but later in the book we'll talk about some of the more advanced debugging features.
Since you ran the program in the simulator, the console should be up. Here's what ours looks like:
The toolbar contains typical debugging commands, like stopping your application, restarting it, and continuing after hitting a breakpoint.
The console has the information about what happened that caused our application to be shut down. It doesn't tell us why it happened, though...
The console tells us that our app was shut down because of an uncaught exception, and what that exception was.
The console also gives us a stack trace of where our application was, but there's a much better view of that coming up in a second...
The console is a very powerful debugging tool. Some of the best debugging techniques involve well-placed logging messages using NSLog(...). This information is printed into the console and can help you diagnose problems quickly. The console isn't just read-only, though; it is your window into your running application. We'll see log messages displayed in the console, and when your application hits a breakpoint, you'll be placed at the console prompt. From there you can use debugging commands like print
, continue
, where
, up
, and down
to inspect the state of your application.
The console debugger is actually the open source gdb prompt, so nearly all gdb commands work here.
In this case, we're dealing with a nearly dead application, but the idea is the same. Since DrinkMixer has crashed, Xcode provides you with the basic information of what went wrong. In our case, an "unrecognized selector" was sent to an object. Remember that a selector is basically a method call—it means that some code is trying to invoke methods on an object and those methods don't exist.
The console prompt lets you interact with your application at the command line.
But Xcode doesn't stop at the command line. It has a full GUI debugger built right in. Let's take a look...
So far we've used Xcode to write code and compile and launch our applications. Its usefulness doesn't stop once we hit the "Build and Debug" button. First, we can set breakpoints in our code to let us keep an eye on what's going on. Simply click in the gutter next to the line where you want to set a breakpoint. Xcode will put a small blue arrow next to the line and when your application gets to that line of code, it will stop and let you poke around using the console.
This switch indicates whether the breakpoints are on or not.
When the breakpoints are on, you'll get this cool can of bug spray icon...
Once your app hits a breakpoint, Xcode will insert Step Into, Step Over, Continue, and Debugger buttons to let you walk through your code.
To set a breakpoint, just click here.
Click on the small bug spray icon or press Shift-
The debugger shows your code and also adds a stack view and a window to inspect variables and memory. When you click on a stack frame, Xcode will show you the line of code associated with that frame and set up the corresponding local variables. There isn't anything in the debugger window you couldn't do with the console, but this provides a nice GUI on top of it.
Here are the Step and Continue buttons to let you walk through your code.
Here's the stack from your app at the current breakpoint (or crash...). If you click on a frame, Xcode will show you the corresponding code.
Xcode shows you your app's variables (local, global, etc.) in this view.
Here's our uncaught exception for the unrecognized selector again...
Our application is crashing, and it's not at the array loading code. Open up the debugger and click on the topmost frame that contains our code. It will show you the line that's causing the problem... see what's wrong?
To be continued...
MultipleViewscross
Take what you've learned about the navigation controller and multiple views to fill in the blanks.
Across
3. The set of views that the nav controller deals with.
6. Dictionaries use __________ to organize data.
8. The screen that gives you output from the app.
9. A template that combines a table view and nav controls.
10. Has cells that need to be customized to work.
Down
1. A more versatile way to manage data beyond an array.
2. DrinkMixer is this type of app.
4. To use a new class you need to ___________ it.
5. The @ symbol is shorthand for creating one of these.
7. A tool in Xcode to help fix broken code.
Get Head First iPhone Development now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.