We’ve written a cool app with one view, but anyone who’s used a smartphone knows that most apps aren’t like that. Some of the more impressive iOS apps out there do a great job of working with 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 Contacts apps. Only we’re going to do it with a twist...
This chapter is about apps with more than one view. What views would you need to have for a bartending app?
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 detail view. Once our user has seen enough, they’re going to want to go back to the drink list.
For this app, we’re going to use a Navigation-based project. The navigation template comes with a lot of functionality built in, including a navigation controller, which handles transitions between views, and the ability to deal with hierarchical data. Hierarchical data means there’s layers to it, and each view gives you more detail than the previous one.
The navigation template comes with a navigation controller and a root view that the controller displays on startup. That root view is set up with a table view by default, and that works great for our app, so we’ll keep it that way. A table view is typically used for listing items, one of which can then be selected for more details about that item.
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.
Remember that MVC pattern from Chapter 1? Now we’re adding in the Model piece with a datasource for our drinks list.
Look through some of the apps you have on your device. What are some of the most customized table views you can find? Are they using sections? Are they grouped? How did they lay out their cells?
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.
In this part of the MVC pattern, the datasource (M) updates the view (V) whenever it has to scroll to a new row.
Get ready to start typing...
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 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.
All of the built-in types we’ve been using, like NSArray and NSString, can be loaded or saved from plists automatically. They do this through the NSCoding protocol. We can take advantage of this and move our drink list out of our source code and into a plist.
Changing the array initialization code to use the plist is remarkably easy. Most Cocoa collection types like NSArray and 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 application-specific information and files. Add the bolded code below to your RootViewController.m file.
Creating your detail view will complete the app.
The entire list of drinks is great, but Sam still needs to know what goes in those views 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 and then show our detailed view. The detail view follows the same MVC pattern as our other views.
We sketched out the detail view earlier—but let’s look more closely at what we’re about to build.
Let’s start building...
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.
Here’s where things get interesting: our RootViewController is our delegate, so it needs to hand off control to a new View Controller to push the detail view onto the screen. How do you think we should handle that?
The only piece left to create is the View Controller for the detail view. 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:
[[DrinkDetailViewController alloc] initWithNibName:@"DrinkDetailView Controller" bundle:nil];
Once we’ve created 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 implementing the callback in the delegate and creating the new View Controller to push onto the stack:
Let’s try this out...
Yep, 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 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.
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 signs in the gutter of the Xcode editor. Two minutes spent investigating a warning can save hours of debugging time later.
Some common warning culprits:
Now that iOS 4.3 is out, code that uses deprecated 2.0 or 3.0 properties triggers warnings.
Sending a message to an object that it doesn’t claim to understand (from a typo or an autocompletion error) will trigger warnings. Your app will compile, but will likely end up in a runtime exception when that code is executed.
That’s not our problem, though: at this point our code is (at least it 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, Xcode is going to bring up the debugging pane at the bottom of the editor. You’ll probably want to resize it to make the log easier to review.
Xcode 4 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 gdb (GNU debugger) 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.
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.
Slide the scrubber all the way to the right to see the full stack from the app...
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.
You’ve got Chapter 4 under your belt and now you’ve added multiple views and the Navigation Controller to your toolbox.