Chapter 3. A Super Start: Adding, Displaying, and Deleting Data

Well, if that last chapter didn't scare you off, then you're ready to dive in and move beyond the basic template we explored in Chapter 2.

In this chapter, we're going to create an application designed to track some superhero data. Our application will be based on the Window-based Application template. We'll use the data model editor to design our superhero entity. And then we'll create a new controller class, derived from UIViewController, that will allow us to add, display, and delete superheroes. In Chapter 4, we'll extend our application further and add code to allow the user to edit their superhero data.

Take a look at Figure 3-1 to get a sense of what our app will look like when it runs. Looks a lot like the template app. The major differences lie in the entity at the heart of the application and in the addition of a tab bar at the bottom of the screen. Let's get to work.

The SuperDB application as it will look once we've finished this chapter

Figure 3.1. The SuperDB application as it will look once we've finished this chapter

Setting up the Xcode Project

Time to get our hands dirty. Launch Xcode if it's not open, and type

Setting up the Xcode Project
Our dear old friend, Xcode's new project assistant

Figure 3.2. Our dear old friend, Xcode's new project assistant

In the last chapter, we started with the Navigation-based Application template. When you create your own navigation applications, that's a good template to use, as it gives you a lot of the code you're likely to need in your application. However, to make it easier to explain where to add or modify code and also to reinforce your understanding of how applications are constructed, we're going to build the SuperDB application from scratch, just as we did throughout most of Beginning iPhone 3 Development (Apress, 2009).

Select Window-based Application, and make sure that the Use Core Data for storage check box is checked. When prompted for a project name, type in SuperDB.

When the project window appears, expand both the Classes and the Resources groups to make it easier to get to the main files with which we'll be working.

Application Architecture

As you can see from Figure 3-1, we're going to create an application with both a tab bar and a navigation controller. Before we start writing code, we need to put a little thought into our application's structure. We need to know, for example, whether our application's root view controller will be a navigation controller, tab bar controller, or something else entirely.

There's not a single right architecture for every application. One obvious approach would be to make the application's root view controller a UITabBarController, and then add a separate navigation controller for each tab. In a situation where each tab corresponds to a completely different view showing different types of data, that approach would make perfect sense. In Beginning iPhone 3 Development, in Chapter 7, we used that exact approach because every single tab corresponded to a different view controller with different outlets and different actions.

In our case, however, we're going to implement two tabs (with more to be added in later chapters), but each tab will show exactly the same data, just ordered differently. When one tab is selected, the table will be ordered by the superhero's name. If the other tab is selected, the same data will be shown, ordered by the superhero's secret identity.

Regardless of which tab is selected, tapping a row on the table will do the same thing: drill down to a new view where you can edit the information about the superhero you selected (which we will add in the next chapter). Regardless of which tab is selected, tapping the add button will add a new instance of the same entity. When you drill down to another view to view or edit a hero, the tabs are no longer relevant.

For our application, the tab bar is just modifying the way the data in a single table is presented. There's no need for it to actually swap in and out other view controllers. Why have multiple navigation controller instances all managing identical sets of data and responding the same way to touches? Why not just use one table controller, and have it change the way it presents the data based on which tab is selected? That's the approach we're going to take in this application. As a result, we won't be using UITabBarController at all.

Our root view controller will be a navigation controller, and we'll use a tab bar purely to receive input from the user. The end result that is shown to the user will be identical to what they'd see if we created separate navigation controllers and table view controllers for each tab, but behind the scenes, we'll be using less memory and won't have to worry about keeping the different navigation controllers in sync with each other.

Our application's root view controller will be an instance of UINavigationController. We'll create our own custom view controller class, HeroListViewController, to act as the root view controller for this UINavigationController. HeroListViewController will display the list of superheroes along with the tabs that control how the heroes are displayed and ordered.

Here's how the app will work. When the application starts, the UINavigationController instance is created from the nib file and the navigation controller's view is added as a subview to the application's window so it can be seen. The rest of the window will be taken up by a content pane for its subcontroller views. Next, the instance of HeroListViewController will be loaded from the nib, and the view from its associated nib file will be added as a subview to the navigation controller's content pane. This view (the one associated with HeroListViewController) contains our tab bar and our superhero table view.

In Chapter 4, we'll add a table view controller into the mix that implements a detail superhero view. When the user taps on a superhero in the superhero list, this detail controller will be pushed onto the navigation stack and its view will temporarily replace the HeroListViewController's view in the UINavigationController's content view. No need to worry about the detail view now, we just wanted you to see what's coming.

Modifying the Application Delegate Interface

Given our approach, we need to declare an outlet to our application's root view controller on our application delegate. Single-click on SuperDBAppDelegate.h and add the code in bold:

@interface SuperDBAppDelegate : NSObject <UIApplicationDelegate> {
    NSManagedObjectModel *managedObjectModel;
    NSManagedObjectContext *managedObjectContext;
    NSPersistentStoreCoordinator *persistentStoreCoordinator;

    UIWindow *window;

    UINavigationController  *navController;
}

@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, retain, readonly) NSManagedObjectContext
    *managedObjectContext;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator
    *persistentStoreCoordinator;

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController *navController;

- (NSString *)applicationDocumentsDirectory;
@end

As you probably realized, the navController outlet will point to an instance of UINavigationController that will act as our application's root view controller. Other view controllers will be pushed onto the navigation stack when they need to be displayed, and will be popped off of the stack when they are done.

Adding to the Application Delegate Implementation

Before we head over to Interface Builder, let's quickly finish up with our Application delegate by adding the following code at the beginning of SuperDBAppDelegate.m:

#import "SuperDBAppDelegate.h"

@implementation SuperDBAppDelegate

@synthesize window;
@synthesize navController;

#pragma mark -
#pragma mark Application lifecycle

- (void)applicationDidFinishLaunching:(UIApplication *)application {

    // Override point for customization after app launch
    [window insertSubview:navController.view atIndex:0];
    [window makeKeyAndVisible];
}

...

There shouldn't be too much there that's unfamiliar to you. We synthesize our new property, just as we always do. In our applicationDidFinishLaunching: method, we add the view property from navController, our application's root view controller, as a subview of contentView so that it will be displayed to the user.

Now, scroll down to the bottom of SuperDBAppDelegate.m. We need to add a few lines to the dealloc method to make sure we're being good memory citizens. Make the following additions at the bottom of the file:

...

- (void)dealloc {
    [managedObjectContext release];
    [managedObjectModel release];
    [persistentStoreCoordinator release];

    [window release];

    [navController release];

    [super dealloc];
}
@end

Make sure you save both SuperDBAppDelegate.h and SuperDBAppDelegate.m before continuing.

Creating the Table View Controller

Our application's root view controller is going to be a stock UINavigationController, so we don't need to define a class for the app's root view controller, but we do need to create a controller class to display the list of heroes and act as the root of the navigation controllers' stack. Even though we will be using a table to display the list of heroes, we're not going to subclass UITableViewController. Because we also need to add a tab bar to our interface, we're going to create a subclass of UIViewController and create our interface in Interface Builder. The table that will display the list of heroes will be a subview of our view controller's content pane.

Single-click the Classes folder in the Groups & Files pane, then type

Creating the Table View Controller

When the new file assistant pops up (Figure 3-3), select Cocoa Touch Class from under the iPhone OS heading in the upper-left pane, then select UIViewController subclass from the upper-right pane. Now make sure the UITableViewController subclass check box is not checked, but the check box labeled With XIB for user interface check box is checked since, unlike with most table-based views, we will need a nib file. With that done, click the Next button.

Selecting the Objective-C subclass template in the new file assistant

Figure 3.3. Selecting the Objective-C subclass template in the new file assistant

When prompted for a filename, type in HeroListViewController.m and make sure the check box labeled Also create "HeroListViewController.h" is checked. Press return to add the files to your project. After the files are created, click and drag HeroListViewController.xib from the Classes folder, where Xcode created it, to the Resources folder where it belongs.

For now, that's all we need in this controller class. In order to create an instance of the class in Interface Builder, we first needed the class definition to exist in Xcode.

Setting up MainWindow.xib

Interface Builder should now be open and should look something like Figure 3-4. You're probably well-acquainted with Interface Builder by now, but let's just quickly review the names of the various windows so that we're all on the same page. The top-left window, the one with MainWindow.xib in the title bar, is the nib file's main window. Below that, the window with the imaginative name of Window represents our application's one and only instance of UIWindow. Double-clicking the Window icon in the nib file's main window will reopen this if it gets closed.

The window with the small title bar to the right of the nib's main window is the context-sensitive Inspector where you can change the attributes of whatever item is currently selected in the active window. And finally, the right-most window is the Library, which contains pre-configured items that you can add to a nib.

MainWindow.xib in Interface Builder

Figure 3.4. MainWindow.xib in Interface Builder

In the library, select the Controllers folder in the top-most pane (inside Library, then inside Cocoa Touch). With Controllers selected in the top pane, look in the middle pane for the Navigation Controller icon (Figure 3-5). Drag one of these to your nib file's main window. Once you do that, your nib's main window will gain an additional icon called Navigation Controller (or Navigation Co... if you're in icon view mode, which truncates longer names), and a new window should have just popped up (Figure 3-6).

The Navigation Controller icon. Depending on the version of Interface Builder you are using, the Library may default to displaying items in one of two ways. You might see just the icon (left), or the icon and a short description (right). You can change how the library items are displayed by right-clicking on the middle pane.

Figure 3.5. The Navigation Controller icon. Depending on the version of Interface Builder you are using, the Library may default to displaying items in one of two ways. You might see just the icon (left), or the icon and a short description (right). You can change how the library items are displayed by right-clicking on the middle pane.

Adding a Navigation Controller to your nib causes a new window to pop up

Figure 3.6. Adding a Navigation Controller to your nib causes a new window to pop up

The new window has a grey rounded rectangle with a dashed outline labeled View and a title of Root View Controller. This is Interface Builder's way of reminding us that a navigation controller needs at least one child view controller in order to function. We can set the root view controller right here in Interface Builder. The easiest way to do this is to put our nib's main window in list mode by clicking the middle of the three View Mode icons (Figure 3-7).

The nib's main window in list view mode

Figure 3.7. The nib's main window in list view mode

With your nib in list view mode, you should notice that Navigation Controller has a disclosure triangle next to it. That means it has sub-items of some form. Different items can contain different types of sub-items. Instances of view classes, for example, can contain subviews. View controller classes generally have either the views they control or the subordinate view controllers they're responsible for managing (or both). Expand Navigation Controller by single-clicking its disclosure triangle. Underneath it, you'll find a navigation bar instance, and a view controller with the rather long and unwieldy name of View Controller (Root View Controller). The view controller represents the navigation controller's root view controller. As we said earlier, HeroListViewController was designed to act as the navigation controller's root view controller. We need to change the class of the root view controller to HeroListViewController.

Single-click View Controller (Root View Controller) and press

The nib's main window in list view mode
The identity inspector allows us to change the underlying class for the navigation controller's root view controller to our custom controller class.

Figure 3.8. The identity inspector allows us to change the underlying class for the navigation controller's root view controller to our custom controller class.

Now we've got a navigation controller and an instance of our custom controller class in our nib.

Connecting the Outlets

Earlier we created an outlet in our application delegate for the navigation controller. We've added an instance of UINavigationController to our nib, so let's connect the outlet. Control-drag from SuperDB App Delegate in the nib's main window to the Navigation Controller also in the nib's main window. When the black menu pops up, select the outlet called navController to connect that outlet.

And with that, we have received final clearance to land our nib. Save and head on back to Xcode to pick up your luggage.

Designing the Data Model

As we discussed in Chapter 2, Xcode's data model editor is where you design your application's data model. In your project window's Resources group, single-click on SuperDB.xcdatamodel. This should bring up the data model editor (Figure 3-9).

The empty data model editor awaiting your application's data model.

Figure 3.9. The empty data model editor awaiting your application's data model.

Unlike the template we used in Chapter 2, this template provides us with a completely empty data model, so we can just dive right in and start building without deleting anything. The first thing we need to add to our data model is an entity. Remember, entities are like class definitions. Although they don't store any data themselves, without at least one entity in your data model, your application won't be able to store any data.

Adding an Entity

Since the purpose of our application is to track information about superheroes, it seems logical that we're going to need an entity to represent a hero. We're going to start off simple in this chapter and track only a few pieces of data about each hero: their name, secret identity, date of birth, and sex. We'll add more data elements in future chapters, but this will give us a basic foundation upon which to build.

In the entity pane, which is the upper-left pane of the data model editor, you should notice buttons with a plus and a minus icon in the lower-left corner (Figure 3-10). As you might have guessed, the button with the plus icon adds a new entity to the data model, and the button with the minus icon removes the currently selected one. Since there's no entity to delete, the minus button is disabled. Click the plus button now to add a new entity.

The plus and minus buttons in the entity pane allow you to add and remove entities from the data model

Figure 3.10. The plus and minus buttons in the entity pane allow you to add and remove entities from the data model

As soon as you click the plus button, a new entity, named Entity, should appear in the entity pane. This entity should be selected for you automatically, which means that the detail pane in the upper-right corner of the data model editor lists details about this new entity and the entity will be selected in the editing pane at the bottom of the data model editor (Figure 3-11).

Editing the New Entity

Now that you've now added an entity to your data model, you'll need to change its name. The easiest way to do that is to change it in the detail pane. Conveniently enough, the Name text field in the detail pane is highlighted and has the focus, so you can just start typing the new name to change the entity's name. Type Hero.

Below the Name field in the detail pane is a text field called Class. Leave this at the default value of NSManagedObject. In Chapter 6, you'll see how to use this field to create custom subclasses of NSManagedObject to add functionality.

Below that is a pop-up menu labeled Parent. Within a data model, you have the ability to specify a parent entity, which is very similar to subclassing in Objective-C. When you specify another entity as your parent, the new entity receives all the properties of Parent along with any additional ones that you specify.

After clicking the plus button in the entity pane, the entity pane gets a new selected row called Entity, the diagram view shows the new entity as a rounded rectangle, and the detail pane shows information about the selected entity

Figure 3.11. After clicking the plus button in the entity pane, the entity pane gets a new selected row called Entity, the diagram view shows the new entity as a rounded rectangle, and the detail pane shows information about the selected entity

Below the Parent pop-up menu is a check box called Abstract. This check box allows you to create an entity that cannot be used to create managed objects at runtime. The reason you might create an abstract entity is if you have several properties that are common to multiple entities. In that case, you might create an abstract entity to hold the common fields and then make every entity that uses those common fields a child of that abstract entity. Doing that would mean that if you needed to change those common fields, you'd only need to do it in one place.

Leave the parenting pop-up set to No Parent Entity and leave the Abstract check box unchecked.

Note

You may be wondering about the button bar in the upper-left of the detail pane. These buttons give you access to more advanced configuration parameters that are only rarely used. We won't be changing any of the configuration options except those visible when the General button is selected (the default button, the one we're on now).

If you're interested in finding out more about these advanced options, you can read more about them in the Core Data Programming Guide at http://developer.apple.com/documentation/Cocoa/Conceptual/CoreData/ and the Core Data Model Versioning and Data Migration Guide at http://devworld.apple.com/documentation/Cocoa/Conceptual/CoreDataVersioning/index.html

Adding Attributes to the New Entity

Now that we have an entity, we have to give it attributes in order for managed objects based on this entity to be able to store any data. For this chapter, we need four attributes: name, secret identity, birth date, and sex.

In the data model editor, to the right of the entity pane is the property pane. This is where you can add properties, including attributes, to the currently selected entity. In the lower-left of the property pane, you should see buttons similar to the ones in the lower-left of the entity pane. Because there is more than one type of property, the button with the plus on it also has a little triangle on it as well. This indicates that when you click the button, you will get a pop-up menu asking you to select exactly which type of property you want to add. Let's add our four attributes now.

Adding the Name Attribute

Single-click on the plus button in the property pane. Once you click on it, you will be presented with a drop-down menu that looks like Figure 3-12. Since we want to add an attribute, select Add Attribute from the menu.

Clicking the plus button in the property pane gives you a menu from which you can select the type of property you wish to add

Figure 3.12. Clicking the plus button in the property pane gives you a menu from which you can select the type of property you wish to add

Editing the Attribute

The Hero entity should now have an attribute called newAttribute. Just as when you created a new entity, the newly added attribute has been automatically selected for you, which also causes its information to be displayed in the detail pane. Also just like before, the Name field should have focus, so you can just type the new name for the attribute. Type name now so that your detail pane looks like Figure 3-13.

The detail pane after typing the new attribute's name

Figure 3.13. The detail pane after typing the new attribute's name

Tip

It's not an accident that we chose to start our entity Hero with a capital H, but our attribute name with a lowercase n. This is the accepted naming convention for entities and properties. Entities begin with a capital letter, properties begin with a lowercase letter. In both cases, if the name of the entity or property consists of more than one word, the first letter of each new word is capitalized.

Below the Name field are three check boxes: Optional, Transient, and Indexed. If Optional is checked, then this entity can be saved even if this attribute has no value assigned to it. If we uncheck it, then any attempt to save a managed object based on this entity when the name attribute is nil will result in a validation error that will prevent the save. In this particular case, name is the main attribute that we will use to identify a given hero, so we probably want to require this attribute. Single-click the Optional check box to uncheck it, making this field required.

The second check box, Transient, allows you to create attributes that are not saved in the persistent store. They can also be used to create custom attributes that store non-standard data. For now, don't worry too much about Transient. Just leave it unchecked and we'll revisit this check box in Chapter 6.

The final check box, Indexed, tells the underlying data store to add an index on this attribute. Not all persistent stores support indices, but the default store (SQLite) does. The database uses an index to improve search speeds when searching or ordering based on that field. We will be ordering our superheroes by name, so let's check the Indexed check box to tell SQLite to create an index on the column that will be used to store this attribute's data.

Warning

Properly used, indices can greatly improve performance in a SQLite persistent store. Adding indices where they are not needed, however, can actually degrade performance. If you don't have a reason for selecting Indexed, leave it unchecked.

Attribute Types

Every attribute has a type, which identifies the kind of data that the attribute is capable of storing. If you single-click the Type drop-down (which should currently be set to Undefined), you can see the various datatypes that Core Data supports out of the box (Figure 3-14). These are all the types of data that you can store without having to implement a custom attribute, like we're going to do in Chapter 6. Each of the datatypes correspond to an Objective-C class that is used to set or retrieve values and you must make sure to use the correct object when setting values on managed objects.

The datatypes supported by Core Data

Figure 3.14. The datatypes supported by Core Data

The Integer Datatypes

Integer 16, Integer 32, and Integer 64 all hold signed integers (whole numbers). The only difference between these three number types is the minimum and maximum size of the values they are capable of storing. In general, you should pick the smallest-size integer that you are certain will work for your purposes. For example, if you know your attribute will never hold a number larger than a thousand, make sure to select Integer 16 rather than Integer 32 or Integer 64. The minimum and maximum values that these three datatypes are capable of storing is as follows:

Datatype

Minimum

Maximum

Integer 16

−32,768

32, 767

Integer 32

−2,147,483,648

2,147,483,647

Integer 64

−9,223,372,036,854,775,808

9,223,372,036,854,775,807

At runtime, you set integer attributes of a managed object using instances of NSNumber created using a factory method such as numberWithInt:, or numberWithLong:.

The Decimal, Double, and Float Datatypes

The Decimal, Double, and Float datatypes all hold decimal numbers. Double and Float hold floating-point representations of decimal numbers similar to the C datatypes of double and float, respectively. Floating-point representations of decimal numbers are always an approximation due to the fact that they use a fixed number of bytes to represent data. The larger the number to the left of the decimal point, the less bytes there are available to hold the fractional part of the number. The Double datatype uses 64 bits to store a single number while the Float datatype uses 32 bits of data to store a single number. For many purposes, these two datatypes will work just fine. However, when you have data, such as currency, where small rounding errors would be a problem, Core Data provides the Decimal datatype, which is not subject to rounding errors. The Decimal type can hold numbers with up to 38 significant digits stored internally using fixed-point numbers so that the stored value is not subject to the rounding errors that can happen with floating-point numbers.

At runtime, you set Double and Float attributes using instances of NSNumber created using the NSNumber factory method numberWithFloat: or numberWithDouble:. Decimal attributes, on the other hand, must be set using an instance of the class NSDecimalNumber.

The String Datatype

The String datatype is one of the most common attribute types you will use. String attributes are capable of holding text in nearly any language or script since they are stored internally using Unicode. String attributes are set at runtime using instances of NSString.

The Boolean Datatype

Boolean values (YES or NO) can be stored using the Boolean datatype. Boolean attributes are set at runtime using instances of NSNumber created using numberWithBOOL:.

The Date Datatype

Dates and timestamps can be stored in Core Data using the Date datatype. At runtime, Date attributes are set using instances of NSDate.

The Binary Datatype

The Binary datatype is used to store any kind of binary data. Binary attributes are set at runtime using NSData instances. Anything that can be put into an NSData instance can be stored in a Binary attribute. However, you generally can't search or sort on binary datatypes.

The Transformable Datatype

The Transformable datatype is a special datatype that works along with something called a value transformer to let you create attributes based on any Objective-C class, even those for which there is no corresponding Core Data datatype. You would use Transformable datatypes to store a UIImage instance, for example, or to store a UIColor instance. You'll see how Transformable attributes work in Chapter 6.

Setting the Name Attributes's Type

A name, obviously, is text, so the obvious type for this attribute is String. Select String from the Type drop-down. After selecting it, a few new fields will appear in the detail pane (Figure 3-15). Just like Interface Builder's inspector, the detail pane in the data model editor is context-sensitive. Some attribute types, such as the String type, have additional configuration options.

The detail pane after selecting the String type

Figure 3.15. The detail pane after selecting the String type

The Min Length: and Max Length: fields allow you to set a minimum and maximum number of characters for this field. If you enter a number into either field, any attempt to save a managed object that has less characters than the Min Length: or more characters than Max Length: stored in this attribute will result in a validation error at save time.

Note that this enforcement happens in the data model, not in the user interface. Unless you specifically enforce limitations through your user interface, these validations won't happen until you actually save the data model. In most instances, if you enforce a minimum or maximum length, you should also take some steps to enforce that in your user interface. Otherwise, the user won't be informed of the error until they go to save, which could be quite a while after they've entered data into this field. You'll see an example of enforcing this in Chapter 6.

The next field is labeled Reg. Ex.: and that stands for regular expression. This field allows you to do further validation on the entered text using regular expressions, which are special text strings that you can use to express patterns. You could, for example, use an attribute to store an IP address in text and then ensure that only valid numerical IP addresses are entered by entering the regular expression d{1,3}.d{1,3}.d{1,3}. d{1,3}. We're not going to use regular expressions for this attribute, so leave the Reg. Ex. field blank.

Note

Regular expressions are a very complex topic on which many full books have been written. Teaching regular expressions is way beyond the scope of this book, but if you're interested in using regular expressions to do data model-level validation, a good starting point is the Wikipedia page on regular expressions at http://en.wikipedia.org/wiki/Regular_expression, which covers the basic syntax and contains links to many regular expression-related resources.

Finally, you can use the field labeled Default Value: to, well, set a default value for this property. If you type a value into this field, any managed object based on this entity will automatically have its corresponding property set to whatever value you type in here. So, in this case, if you were to type Untitled Hero into this field, any time you created a new Hero managed object, the name property would automatically get set to Untitled Hero. Heck, that sounds like a good idea, so type Untitled Hero into this field. Then, for good measure, save.

Adding the Rest of the Attributes

Our Hero entity needs three more attributes, so let's add them now. Click the plus button in the properties pane again and select Add Attribute once more. Give this one a name of secretIdentity and a type of String. Since, according to Mr. Incredible, every superhero has a secret identity, we'd better uncheck the Optional check box. We will be sorting and searching on secret identity, so check the Indexed box. For Default Value:, type in Unknown. Because we've made the field mandatory by unchecking the Optional check box, it's a good idea to provide a default value. Leave the rest of the fields as is.

Warning

Be sure you enter default values for the name and secretIdentity attributes. If you don't, the program will behave badly. If your program crashes, check to make sure you've saved your source code files and your nib files.

Click the plus button a third time to add yet another attribute, giving it a name of birthdate and a type of Date. Leave the rest of the fields at their default values for this attribute. We may not know the birthdate for all of our superheroes, so we want to leave this attribute as optional. As far as we know now, we won't be doing a lot of searching or ordering on birthdate, so there's no need to make this attribute indexed. We could do some additional validation here by setting a minimum, maximum, or default date, but there really isn't much need. There's no default value that would make sense, and setting a minimum or maximum date would preclude the possibility of an immortal superhero or a time-traveling one, which we certainly don't want to do!

That leaves us with one more attribute for this first iteration of our application: sex. There are a number of ways that we could choose to store this particular piece of information. For simplicity's sake (and because it will help us show you a few helpful techniques in Chapter 6), we're just going to store a character string of either Male or Female. Add another attribute and select a Type of String. Let's leave this as an optional setting—there might just be an androgynous masked avenger or two out there. We could use the regular expression field to limit inputs to either Male or Female but, instead, we're going to enforce that in the user interface by presenting a selection list rather than enforcing it here in the data model.

Guess what? You've now completed the data model for the first iteration of the SuperDB application. Save it and let's go create our controller.

Creating HeroListViewController

If you look back at Figure 3-1, you can see that our application displays a list of heroes, and it can sort that list by either name or secret identity. As we discussed earlier in the chapter, we're using a single controller to handle both of the sort options rather than using separate controllers for each one. In order to retrieve the results from our persistent store, we're going to use a fetched results controller just as the template code we looked at in the last chapter did. However, we are not using a table view controller, so we have to design our user interface in our nib. Before we do that, though, we should declare the outlets that we're going to need.

Declaring the Fetched Results Controller

Single-click on HeroListViewController.h to bring up the header file for our class. We need to declare our property and instance variable for the fetched results controller, so make the following changes to your file:

#import <UIKit/UIKit.h>

#define kSelectedTabDefaultsKey @"Selected Tab"

enum {
    kByName,
    kBySecretIdentity,
};

@interface HeroListViewController : UIViewController
    <UITableViewDelegate, UITableViewDataSource, UITabBarDelegate,
    UIAlertViewDelegate, NSFetchedResultsControllerDelegate>
{
    UITableView *tableView;
    UITabBar    *tabBar;

@private
NSFetchedResultsController *_fetchedResultsController;
}

@property (nonatomic, retain) IBOutlet UITableView *tableView;
@property (nonatomic, retain) IBOutlet UITabBar *tabBar;
@property (nonatomic, readonly) NSFetchedResultsController
    *fetchedResultsController;

- (void)addHero;
- (IBAction)toggleEdit;

@end

This looks just a little different than what we've done before, so let's discuss what's going on here. First, we define a constant that will be used as a key to store and retrieve a preference value in the user defaults. When our program launches, we want to take the user back to the same tab they were on when they last used the program. This constant will be used to store that information in our application's preferences.

After that, we define an enumeration that gives us constants for the individual tabs used in the tab bar, just to make our code a bit more readable. The number 0 can mean lots of different things in the context of our code, but the constant kByName makes it obvious that this time, it's referring to the tab called By Name.

Next, we conform our class to a whole bunch of protocols. Because we're not subclassing UITableViewController, we have to manually conform to UITableViewDelegate and UITableViewDataSource. We also conform to UITabBarDelegate because we're also going to act as the tab bar delegate. Doing so will cause us to be notified whenever the user selects a new tab without having to utilize action methods.

If we encounter a fatal error, we're going to show the user an alert before quitting, so we have to become the alert's delegate in order to be notified when the alert is dismissed. That's why we also need to conform to UIAlertViewDelegate. The template code just logs errors to the console and quits, but we're going to be a little more user-friendly than that and let the user know when something has gone wrong.

Finally, we conform to NSFetchedResultsControllerDelegate because we're going to be using a fetched results controller and will need to be notified when its data changes.

After that, we create instance variables to serve as outlets for the tab bar and table view. Then, we specify the @private keyword, which indicates that all instance variables that follow have a private scope and cannot be accessed directly by other classes. We then create a private instance variable in which to store our fetched results controller. Notice that we've called the instance variable _fetchedResultsController, yet if you look down a few lines later, the property is actually named fetchedResultsController, without the underscore.

By default, properties expect their underlying instance variable to have the same name as the property. However, that is just the default behavior and is not required. When you synthesize your property in the implementation file using the @synthesize keyword, you can specify the name of the underlying instance variable to be used to store the property's data. The specified name can be anything at all. It doesn't need to be related to or similar to the property name at all.

When we synthesize this property, we'll use this line of code:

@synthesize fetchedResultsController=_fetchedResultsController;

The property name goes immediately after the @synthesize keyword, just as always, but it is then followed by an equal sign and then the name of the instance variable to be used. This particular convention of using the same name as the property but prefixed with an underscore is one you see a lot, even in Apple's sample code. Some programmers use this naming convention for all of their properties. We tend to use it only when there's a specific reason to not want other objects mucking with an instance variable.

This naming convention should prevent us from accidentally confusing the property and instance variable in our code. By using different names for each, we are far less likely to access the instance variable directly when we intend to use the property.

You might remember from the last chapter that our fetchedResultsController was lazily loaded. As a result, it is critical that references to the fetchedResultsController be done through the accessor, since the accessor will make sure that our fetchedResultsController was properly loaded. We're going to be doing the same thing in this chapter. This naming convention and the use of the @private keyword will help prevent unintentional direct access to the instance variable that could cause problems if the fetched results controller hasn't been loaded by an earlier use of the accessor.

Note

You may hear developers claim that using the underscore prefix is reserved by Apple and that you shouldn't use it. This is a misconception. Apple does, indeed, reserve the underscore prefix for the names of methods. It does not make any similar reservation when it comes to the names of instance variables. You can read Apple's naming convention for instance variables, which makes no restriction on the use of the underscore, here:

http://developer.apple.com/documentation/Cocoa/Conceptual/CodingGuidelines/Articles/NamingIvarsAndTypes.html

Notice that the fetchedResultsController property is declared with the readonly keyword. We will be lazily loading the fetched results controller in the accessor method. We do not want other classes to be able to set fetchedResultsController, so we declare it readonly to prevent that from happening.

Drag Two Icons to Your Project

Before writing our implementation of HeroListViewController, we need to head over to Interface Builder to design the interface and connect our outlets. Before we do that, however, you need to copy two image files into your Xcode project so that they'll be available to you in Interface Builder. If you look in the project archive that accompanies this book, in the 03 - SuperDB folder, you'll find files called name_icon.png and secret_icon.png. These are the images that you will use on the two tabs. Add them both to your project in the Resources group. Once you've done that, you can double-click HeroListViewController.xib to open up Interface Builder.

Designing the HeroListViewController Interface

When the nib file opens, the View window should show up. If it doesn't, double-click the View icon in the nib's main window to open it. We need to add a tab bar and a table view to our nib, and then make the connections.

Let's add the tab bar first. Look in the Library for a tab bar (Figure 3-16). Make sure you're grabbing a tab bar and not a tab bar controller. We only want the user interface item.

The tab bar in the Library

Figure 3.16. The tab bar in the Library

Drag a tab bar from the library to the window called View, and place it snugly in the bottom of the window, as we've done in Figure 3-17.

The tab bar placed snugly against the bottom of the screen

Figure 3.17. The tab bar placed snugly against the bottom of the screen

The default tab bar has two tabs, which is exactly the number we want. Let's change the icon and label for each. With the tab bar still selected, click on the star above Favorites and then press

The tab bar placed snugly against the bottom of the screen

If you've correctly selected the tab bar item, the inspector window should have the title Tab Bar Item Attributes and the Identifier pop-up should say Favorites. In the attribute inspector, give this tab a Title of By Name, and an Image of name_icon.png (Figure 3-18). Now click on the three dots above the word More on the tab bar to select the right tab. Using the inspector, give this tab a Title of By Secret Identity and an Image of secret_icon.png.

Setting the attributes of the left tab

Figure 3.18. Setting the attributes of the left tab

Back in the library, look for a Table View (Figure 3-19). Again, make sure you're getting the user interface element, not a Table View Controller. Drag this to the space above the tab bar. It should resize automatically to fit the space available. After you drop it, it should look like Figure 3-20.

The Table View in the library

Figure 3.19. The Table View in the library

The HeroListViewController interface after dropping the table on it

Figure 3.20. The HeroListViewController interface after dropping the table on it

With the table in place, the HeroListViewController interface is complete, we just need to make the outlet, delegate, and datasource connections. Control-drag from File's Owner to the table view and select the tableView outlet, then control-drag again from File's Owner to the tab bar and select the tabBar outlet. That takes care of the outlet connections. Let's move on to the delegate and datasource connections.

Control-drag twice from the table view to File's Owner, selecting the dataSource outlet one time, and the delegate outlet the other. Then control-drag from the tab bar to File's Owner and select the delegate outlet. Now our controller's outlets are connected, and our controller is the delegate for both the tab bar and table view, and is the data source for the table view as well. Our job here is done. Save the nib and go back to Xcode.

Implementing the Hero View Controller

The implementation of HeroListViewController is going to look a bit like RootViewController from the previous chapter, even though they have different superclasses. Replace the current contents of your HeroListViewController.m file with the following code. Once you've done that, we'll talk about the new stuff it contains.

#import "HeroListViewController.h"
#import "SuperDBAppDelegate.h"

@implementation HeroListViewController
#pragma mark Properties
@synthesize tableView;
@synthesize tabBar;
@synthesize fetchedResultsController = _fetchedResultsController;

#pragma mark -
- (void)addHero {
    NSManagedObjectContext *context =
        [self.fetchedResultsController managedObjectContext];
    NSEntityDescription *entity =
        [[self.fetchedResultsController fetchRequest] entity];
    NSManagedObject *newManagedObject = [NSEntityDescription
        insertNewObjectForEntityForName:[entity name]
        inManagedObjectContext:context];

    NSError *error;
    if (![context save:&error])
        NSLog(@"Error saving entity: %@", [error localizedDescription]);

    // TODO: Instantiate detail editing controller and push onto stack
}

- (IBAction)toggleEdit {
    BOOL editing = !self.tableView.editing;
    self.navigationItem.rightBarButtonItem.enabled = !editing;
    self.navigationItem.leftBarButtonItem.title = (editing) ?
        NSLocalizedString(@"Done", @"Done") : NSLocalizedString(@"Edit", @"Edit");
    [self.tableView setEditing:editing animated:YES];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        UIAlertView *alert = [[UIAlertView alloc]
            initWithTitle:NSLocalizedString(@"Error loading data",
                @"Error loading data")
            message:[NSString stringWithFormat:NSLocalizedString(
                @"Error was: %@, quitting.", @"Error was: %@, quitting."),
                [error localizedDescription]]
            delegate:self
            cancelButtonTitle:NSLocalizedString(@"Aw, Nuts", @"Aw, Nuts")
            otherButtonTitles:nil];
        [alert show];

                }

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSInteger selectedTab = [defaults integerForKey:kSelectedTabDefaultsKey];
    UITabBarItem *item = [tabBar.items objectAtIndex:selectedTab];
    [tabBar setSelectedItem:item];
}

 - (void)viewDidAppear:(BOOL)animated {
    UIBarButtonItem *editButton = self.editButtonItem;
    [editButton setTarget:self];
    [editButton setAction:@selector(toggleEdit)];
self.navigationItem.leftBarButtonItem = editButton;

    UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
        target:self
        action:@selector(addHero)];
    self.navigationItem.rightBarButtonItem = addButton;
    [addButton release];
}

- (void)viewDidUnload {
    self.tableView = nil;
    self.tabBar = nil;
}

- (void)dealloc {
    [tableView release];
    [tabBar release];
    [_fetchedResultsController release];
    [super dealloc];
}

#pragma mark -
#pragma mark Table View Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)theTableView {
    NSUInteger count = [[self.fetchedResultsController sections] count];
    if (count == 0) {
        count = 1;
    }
    return count;
}

- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section {
    NSArray *sections = [self.fetchedResultsController sections];
    NSUInteger count = 0;
    if ([sections count]) {
        id <NSFetchedResultsSectionInfo> sectionInfo =
            [sections objectAtIndex:section];
        count = [sectionInfo numberOfObjects];
    }
    return count;
}

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

    static NSString *HeroTableViewCell = @"HeroTableViewCell";

    UITableViewCell *cell = [tableView
        dequeueReusableCellWithIdentifier:HeroTableViewCell];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
            reuseIdentifier:HeroTableViewCell] autorelease];
    }
    NSManagedObject *oneHero = [self.fetchedResultsController
        objectAtIndexPath:indexPath];
NSInteger tab = [tabBar.items indexOfObject:tabBar.selectedItem];
    switch (tab) {
        case kByName:
            cell.textLabel.text = [oneHero valueForKey:@"name"];
            cell.detailTextLabel.text = [oneHero valueForKey:@"secretIdentity"];
            break;
        case kBySecretIdentity:
            cell.detailTextLabel.text = [oneHero valueForKey:@"name"];
            cell.textLabel.text = [oneHero valueForKey:@"secretIdentity"];
        default:
            break;
    }
    return cell;
}

- (void)tableView:(UITableView *)theTableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // TODO: Instantiate detail editing view controller and push onto stack
}

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {

    if (editingStyle == UITableViewCellEditingStyleDelete) {
        NSManagedObjectContext *context = [self.fetchedResultsController
            managedObjectContext];
        [context deleteObject:[self.fetchedResultsController
            objectAtIndexPath:indexPath]];

        NSError *error;
        if (![context save:&error]) {
           NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
           UIAlertView *alert = [[UIAlertView alloc] initWithTitle:
                NSLocalizedString(@"Error saving after delete",
                @"Error saving after delete.")
           message:[NSString stringWithFormat:NSLocalizedString(
                @"Error was: %@, quitting.",@"Error was: %@, quitting."),
               [error localizedDescription]]
           delegate:self
           cancelButtonTitle:NSLocalizedString(@"Aw, Nuts", @"Aw, Nuts")
                                                  otherButtonTitles:nil];
           [alert show];
        }
    }
}

#pragma mark -
#pragma mark Fetched results controller
- (NSFetchedResultsController *)fetchedResultsController {

    if (_fetchedResultsController != nil) {
        return _fetchedResultsController;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
SuperDBAppDelegate *appDelegate = UIApplication
        sharedApplication] delegate];
    NSManagedObjectContext *managedObjectContext = appDelegate.managedObjectContext;

    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Hero"
        inManagedObjectContext:managedObjectContext];

    NSUInteger tab = [tabBar.items indexOfObject:tabBar.selectedItem];
    if (tab == NSNotFound) {
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        tab = [defaults integerForKey:kSelectedTabDefaultsKey];
    }

    NSString *sectionKey = nil;
    switch (tab) {
        case kByName: {
            NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc]
                initWithKey:@"name" ascending:YES];
            NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc]
                initWithKey:@"secretIdentity" ascending:YES];
            NSArray *sortDescriptors = [[NSArray alloc]
                initWithObjects:sortDescriptor1, sortDescriptor2, nil];
            [fetchRequest setSortDescriptors:sortDescriptors];
            [sortDescriptor1 release];
            [sortDescriptor2 release];
            [sortDescriptors release];
            sectionKey = @"name";
            break;
        }
        case kBySecretIdentity:{
            NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc]
                initWithKey:@"secretIdentity" ascending:YES];
            NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc]
                initWithKey:@"name" ascending:YES];
            NSArray *sortDescriptors = [[NSArray alloc]
                initWithObjects:sortDescriptor1, sortDescriptor2, nil];
            [fetchRequest setSortDescriptors:sortDescriptors];
            [sortDescriptor1 release];
            [sortDescriptor2 release];
            [sortDescriptors release];
            sectionKey = @"secretIdentity";
            break;
        }
        default:
            break;

    }
    [fetchRequest setEntity:entity];
    [fetchRequest setFetchBatchSize:20];

    NSFetchedResultsController *frc = [[NSFetchedResultsController alloc]
        initWithFetchRequest:fetchRequest
        managedObjectContext:managedObjectContext
        sectionNameKeyPath:sectionKey
        cacheName:@"Hero"];
    frc.delegate = self;
    _fetchedResultsController = frc;
[fetchRequest release];

    return _fetchedResultsController;
}

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath {
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertRowsAtIndexPaths:[NSArray
                arrayWithObject:newIndexPath]
                withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteRowsAtIndexPaths:[NSArray
            arrayWithObject:indexPath]
            withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeUpdate: {
            NSString *sectionKeyPath = [controller sectionNameKeyPath];
            if (sectionKeyPath == nil)
                break;
            NSManagedObject *changedObject = [controller
                objectAtIndexPath:indexPath];
            NSArray *keyParts = [sectionKeyPath componentsSeparatedByString:@"."];
            id currentKeyValue = [changedObject valueForKeyPath:sectionKeyPath];
            for (int i = 0; i < [keyParts count] - 1; i++) {
                NSString *onePart = [keyParts objectAtIndex:i];
                changedObject = [changedObject valueForKey:onePart];
            }
            sectionKeyPath = [keyParts lastObject];
            NSDictionary *committedValues = [changedObject
                committedValuesForKeys:nil];

            if ([[committedValues valueForKeyPath:sectionKeyPath]
                isEqual:currentKeyValue])
                break;

            NSUInteger tableSectionCount = [self.tableView numberOfSections];
            NSUInteger frcSectionCount = [[controller sections] count];
            if (tableSectionCount != frcSectionCount) {
                // Need to insert a section
                NSArray *sections = controller.sections;
                NSInteger newSectionLocation = −1;
                for (id oneSection in sections) {
                    NSString *sectionName = [oneSection name];
if ([currentKeyValue isEqual:sectionName]) {
                         newSectionLocation = [sections indexOfObject:oneSection];
                        break;
                    }
                }
                if (newSectionLocation == −1)
                    return; // uh oh

                if (!(newSectionLocation == 0 && tableSectionCount == 1) &&
                    [self.tableView numberOfRowsInSection:0] == 0)
                    [self.tableView insertSections:[NSIndexSet
                        indexSetWithIndex:newSectionLocation]
                        withRowAnimation:UITableViewRowAnimationFade];
                NSUInteger indices[2] = {newSectionLocation, 0};
                newIndexPath = [[[NSIndexPath alloc] initWithIndexes:indices
                    length:2] autorelease];
            }
        }
        case NSFetchedResultsChangeMove:
            if (newIndexPath != nil) {
                [self.tableView deleteRowsAtIndexPaths:[NSArray
                    arrayWithObject:indexPath]
                    withRowAnimation:UITableViewRowAnimationFade];
                [self.tableView insertRowsAtIndexPaths: [NSArray
                    arrayWithObject:newIndexPath]
                    withRowAnimation: UITableViewRowAnimationRight];

         }
            else {
                [self.tableView reloadSections:[NSIndexSet
                    indexSetWithIndex:[indexPath section]]
                    withRowAnimation:UITableViewRowAnimationFade];
            }
            break;
        default:
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller
  didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex
     forChangeType:(NSFetchedResultsChangeType)type {
    switch(type) {
        case NSFetchedResultsChangeInsert:
            if (!(sectionIndex == 0 && [self.tableView numberOfSections] == 1) &&
                [self.tableView numberOfRowsInSection:0] == 0)
                [self.tableView insertSections:[NSIndexSet
                    indexSetWithIndex:sectionIndex]
                    withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete:
            if (!(sectionIndex == 0 && [self.tableView numberOfSections] == 1) &&
                [self.tableView numberOfRowsInSection:0] == 0)
                [self.tableView deleteSections:[NSIndexSet
                    indexSetWithIndex:sectionIndex]
                     withRowAnimation:UITableViewRowAnimationFade];
break;
        case NSFetchedResultsChangeMove:
            break;
        case NSFetchedResultsChangeUpdate:
            break;
        default:
            break;
    }
}

#pragma mark -
#pragma mark UIAlertView Delegate
- (void)alertView:(UIAlertView *)alertView
    didDismissWithButtonIndex:(NSInteger)buttonIndex {
    exit(-1);
}

#pragma mark -
#pragma mark Tab Bar Delegate
- (void)tabBar:(UITabBar *)theTabBar didSelectItem:(UITabBarItem *)item {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSUInteger tabIndex = [tabBar.items indexOfObject:item];
    [defaults setInteger:tabIndex forKey:kSelectedTabDefaultsKey];

    _fetchedResultsController.delegate = nil;
    [_fetchedResultsController release];
    _fetchedResultsController = nil;

    NSError *error;
    if (![self.fetchedResultsController performFetch:&error])
        NSLog(@"Error performing fetch: %@", [error localizedDescription]);
    [self.tableView reloadData];
}

@end

Okay, that was a lot of code. As you were typing it, a lot of it probably looked familiar. Let's start at the top and work our way down until we've covered all the new stuff.

The first few lines are pretty straightforward. We import our header file, and also import the header file from our application delegate because we'll be using our application delegate in a few methods.

#import "HeroListViewController.h"
#import "SuperDBAppDelegate.h"

@implementation HeroListViewController

Then we synthesize our properties, making sure to identify the instance variable that backs the fetchedResultsController since its underlying instance variable has a different name:

#pragma mark Properties
@synthesize tableView;
@synthesize tabBar;
@synthesize fetchedResultsController = _fetchedResultsController;

After that, we first have our method for adding new heroes. This method is nearly identical to the insertNewObjects: method from last chapter. If save: encounters an error, it will return NO and we'll send an error to the console.

- (void)addHero {
    NSManagedObjectContext *context =
        [self.fetchedResultsController managedObjectContext];
    NSEntityDescription *entity =
        [[self.fetchedResultsController fetchRequest] entity];
    NSManagedObject *newManagedObject = [NSEntityDescription
        insertNewObjectForEntityForName:[entity name]
        inManagedObjectContext:context];

    NSError *error;
    if (![context save:&error])
        NSLog(@"Error saving entity: %@", [error localizedDescription]);

    // TODO: Instantiate detail editing controller and push onto stack
}

Note

You will get a warning about the unused variable newManagedObject when you compile this code. We actually need this line of code because it creates a new managed object and inserts that object into the context. We don't use the pointer returned by this call, and that's why we get the warning. Normally, we just wouldn't save the returned value, but we will be using this pointer in Chapter 4 when we expand our application. So live with the warning for now and know that we will be making use of newManagedObject in the next chapter.

Notice that comment at the end of the method? Some comments that begin with certain strings have special meaning in Xcode, and this is one of those strings. A comment that begins with // TODO: will be included in the function pop-up menu (Figure 3-21). These comments are designed to work as a reminder to ourselves to come back later and finish this incomplete piece of functionality. In this case, it's a reminder to instantiate the detail editing pane that will allow the user to edit the newly added hero and push it onto the navigation stack, which we'll do in the next chapter.

Tip

There are other special comments that will show up in the function pop-up menu in addition to // TODO:. If you want to indicate a problem that needs to be fixed, you can insert a comment that begins with // FIXME:. Comments beginning with either // ???: or // !!!: will also show up in the function pop-up, the former typically being used to indicate a question or something puzzling in the code, and the latter typically being used to mark something urgent or surprising in the code. You can also just put an entry in the function menu using comments that begin with // MARK:, which will cause anything on the line after the colon to show up in the function menu the way using #pragma mark does.

Certain comments will show up in the function pop-up menu, such as this TODO comment we added to our code

Figure 3.21. Certain comments will show up in the function pop-up menu, such as this TODO comment we added to our code

Next comes toggleEdit, our action method for turning on and off our table view's edit mode. In addition to simply toggling the table view's edit mode, we also have a bit of housekeeping to attend to, to make sure the user interface works as the user expects it to. Because we've subclassed UIViewController instead of UITableViewController, we have to maintain the Edit button's label ourselves. We change the title of the edit button to either Edit or Done based on whether the table is in editing mode or not. We also hide the right nav bar button, which is used to add new rows, based on whether editing mode is being turned on or off. We don't want the user to be able to add new rows while we're in edit mode.

- (IBAction)toggleEdit {
    BOOL editing = !self.tableView.editing;
    self.navigationItem.rightBarButtonItem.enabled = !editing;
    self.navigationItem.leftBarButtonItem.title = (editing) ?
        NSLocalizedString(@"Done", @"Done") : NSLocalizedString(@"Edit", @"Edit");
    [self.tableView setEditing:editing animated:YES];
}

At first glance, viewDidLoad looks like the version from the template. We start by calling the same method on super, and then we get the fetched results controller and call performFetch:.

- (void)viewDidLoad {
    [super viewDidLoad];
    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {

If an error is encountered, however, we no longer just log and quit. Instead, we show an alert informing the user of the error. We still log more detailed information to the console, and we still quit, but at least we tell the user that we're quitting and why before we do it. The actual command to quit is actually in the alert view delegate method alertView:didDismissButtonWithIndex:, which will cause the program to quit after the user dismisses the alert.

UIAlertView *alert = [[UIAlertView alloc]
            initWithTitle:NSLocalizedString(@"Error loading data",
                @"Error loading data")
            message:[NSString stringWithFormat:@"Error was: %@, quitting.",
                [error localizedDescription]]
            delegate:self
            cancelButtonTitle:NSLocalizedString(@"Aw, Nuts", @"Aw, Nuts")
            otherButtonTitles:nil];
        [alert show];
    }
}

The viewDidAppear: method is nearly identical to the one from the previous chapter. It makes sure that the edit and add buttons are in the navigation bar.

- (void)viewDidAppear:(BOOL)animated {
    self.navigationItem.leftBarButtonItem = self.editButtonItem;
    UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
        target:self
        action:@selector(addHero)];
    self.navigationItem.rightBarButtonItem = addButton;
    [addButton release];
}

After that, we override setEditing:animated:, which is the method that gets called when the edit button is tapped, or when the user swipes a row.

- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
    self.navigationItem.rightBarButtonItem.enabled = !editing;
    self.navigationItem.leftBarButtonItem.title = (editing) ?
        NSLocalizedString(@"Done", @"Done") :
        NSLocalizedString(@"Edit", @"Edit");
    [self.tableView setEditing:editing animated:animated];
}

The table view delegate and datasource methods are pretty straightforward, so let's skip down to fetchedResultsController. Everything there starts out pretty much the same as the version in the last chapter:

- (NSFetchedResultsController *)fetchedResultsController {
    if (_fetchedResultsController != nil) {
        return _fetchedResultsController;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

    SuperDBAppDelegate *appDelegate = (SuperDBAppDelegate *)[[UIApplication
        sharedApplication] delegate];
    NSManagedObjectContext *managedObjectContext = appDelegate.managedObjectContext;
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Hero"
        inManagedObjectContext:managedObjectContext];

Because our sort descriptors are going to depend on the currently selected tab, we need to find out which tab is currently selected. If no tab is selected, as might be the case if this method is called before the nib has loaded, we'll grab the last used value from preferences.

NSUInteger tab = [tabBar.items indexOfObject:tabBar.selectedItem];
    if (tab == NSNotFound) {
        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
        tab = [defaults integerForKey:kSelectedTabDefaultsKey];
    }

Next, we create sort descriptors and a fetch request much as the template did in the last chapter, only we use different sort descriptors based on the currently selected tab. We're also going to use another feature of the fetched results controller that wasn't used in the template. If we specify a section name keypath when we create our fetched results controller, our fetched results controller will automatically divide the result set into sections. The most common scenario is to simply pass the same key used in the first sort descriptor. So, if you're sorting by name, and pass in @"name" as the section name keypath and sections will automatically be created based on the first letter of the hero's name. We won't be able to see this functionality in action until the next chapter when we add the ability to edit heroes.

Here, we set the sort descriptor and section name keypath based on the currently selected tab:

NSString *sectionKey = nil;
    switch (tab) {
        case kByName: {
            NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc]
                initWithKey:@"name" ascending:YES];
            NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc]
                initWithKey:@"secretIdentity" ascending:YES];
            NSArray *sortDescriptors = [[NSArray alloc]
                initWithObjects:sortDescriptor1, sortDescriptor2, nil];
            [fetchRequest setSortDescriptors:sortDescriptors];
            [sortDescriptor1 release];
            [sortDescriptor2 release];
            [sortDescriptors release];
            sectionKey = @"name";
            break;
        }
        case kBySecretIdentity:{
            NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc]
                initWithKey:@"secretIdentity" ascending:YES];
            NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc]
                initWithKey:@"name" ascending:YES];
            NSArray *sortDescriptors = [[NSArray alloc]
                initWithObjects:sortDescriptor1, sortDescriptor2, nil];
            [fetchRequest setSortDescriptors:sortDescriptors];
            [sortDescriptor1 release];
            [sortDescriptor2 release];
[sortDescriptors release];
            sectionKey = @"secretIdentity";
            break;
        }
        default:
            break;
    }
    fetchRequest setEntity:entity];
    [fetchRequest setFetchBatchSize:20];

    NSFetchedResultsController *frc = [[NSFetchedResultsController alloc]
        initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext
        sectionNameKeyPath:sectionKey
        cacheName:@"Hero"];

After that, we just make self the delegate of the fetched results controller so that we get notified of changes, assign the new fetched results controller to our private instance variable, and then return that instance variable. Notice that we don't release frc. This is intentional. Since we're assigning the controller directly to the instance variable, it does not get retained automatically. That means that _fetchedResultsController already has a retain count of 1, which is what we want.

frc.delegate = self;
    _fetchedResultsController = frc;

    return _fetchedResultsController;
}

Next are the four fetched results controller delegate methods. Our implementation here is exactly the same as we discussed last chapter, so if you're unclear as to what these four methods are doing, go back to Chapter 2 and re-read the section called Working With a Fetched Results Controller.

The alert view delegate method, which gets called when the user dismisses an alert view, does nothing more than quit the application. In this controller, the only reason that we've used alert view is to inform the user of a fatal error.

#pragma mark -
#pragma mark UIAlertView Delegate
- (void)alertView:(UIAlertView *)alertView
    didDismissWithButtonIndex:(NSInteger)buttonIndex {
    exit(-1);
}

Finally, we have the tab bar delegate method tabBar:didSelectItem:, which gets called whenever the user changes the selected tab in our tab bar. In this method, we start by storing the index of the tab the user selected into user defaults. Although tab bar controllers use tab indices to identify which tab is selected, tab bars themselves don't use indices. Instead, we're actually passed the tab bar item that was selected, and we have to determine the index of the tab. It's easy enough to do. UITabBar maintains an array of its items called items. The index of the tab bar item in that array is the tab index, so we can use NSArray's indexOfObject: method to determine it:

#pragma mark -
#pragma mark Tab Bar Delegate
- (void)tabBar:(UITabBar *)theTabBar didSelectItem:(UITabBarItem *)item {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSUInteger tabIndex = [tabBar.items indexOfObject:item];
    [defaults setInteger:tabIndex forKey:kSelectedTabDefaultsKey];

The next thing we do is set the fetched results controller to nil. By doing this, the next time the fetchedResultsController accessor method is called, it will reload the result set from the persistent store using the criteria based on the new tab selection. Before we set it to nil, however, we set its delegate property to nil. We were the fetched results controller's delegate, but once we set it to nil, we don't want to be its delegate any more.

Note

Setting the delegate property to nil when you're done is good form, but if you fail to do it, it usually won't cause any major problems. Although there are a few exceptions in the system, generally speaking, objects do not retain their delegates, so failing to set a delegate to nil won't prevent an object's retain count from reaching zero when it is another object's delegate.

_fetchedResultsController.delegate = nil;
    [_fetchedResultsController release];
    _fetchedResultsController = nil;

After we set the fetched results controller to nil, we then call performFetch:, just like we did in viewDidLoad so that the data gets reloaded immediately based on the new criteria. This is the one situation when it's important to call reloadData when using a fetched results controller. Since we release the old fetched results controller and create a new one, we can't rely on the fetched results controller delegate methods to update the table for us.

NSError *error;
    if (![self.fetchedResultsController performFetch:&error])
        NSLog(@"Error performing fetch: %@", [error localizedDescription]);
    [self.tableView reloadData];
}

And that's pretty much everything.

Let 'Er Rip

Well, what are you waiting for? That was a lot of work; you deserve to try it out. Make sure everything is saved, then select Build and Run from the Build menu in Xcode to try things out.

If everything went okay, when the application first launches, you should be presented with an empty table with a navigation bar at the top and a tab bar at the bottom (Figure 3-22). Pressing the right button in the navigation bar will add a new unnamed superhero to the database. Pressing the Edit button will allow you to delete heroes.

Note

If your app crashed when you ran it, there's a couple of things to look for. First, make sure you saved all your source code and nib files before you ran your project. Also, make sure that you have defaults specified for your hero's name and secretIdentity in your data model editor. If you did that and your app still crashes, try resetting your simulator. Here's how you do that. Bring up the simulator. From the iPhone Simulator menu, select Reset Contents and Settings.... That should do it. In Chapter 5, we'll show you how to ensure that changes to your data model don't cause such problems.

The SuperDB application at launch time

Figure 3.22. The SuperDB application at launch time

Make sure you try out the two tabs and make sure that the display changes when you select a new tab. When you select the By Name tab, it should look like Figure 3-1, but when you select the By Secret Identity tab, it should look like Figure 3-23.

Pressing the Secret Identity tab doesn't change the order of the rows yet, but it does change which value is displayed first

Figure 3.23. Pressing the Secret Identity tab doesn't change the order of the rows yet, but it does change which value is displayed first

Done, but Not Done

In this chapter, you did a lot of work. You saw how to set up a navigation-based application that uses a tab bar, and learned how to design a basic Core Data data model by creating an entity and giving it several attributes.

This application isn't done, but you've now laid a solid foundation on which to move forward. When you're ready, turn the page, and we'll create a detail editing page to allow the user to edit their superheroes.

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

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