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.
Time to get our hands dirty. Launch Xcode if it's not open, and type
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.
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.
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.
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.
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
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.
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.
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.
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).
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.
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).
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
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.
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.
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).
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.
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.
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).
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.
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.
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
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.
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.
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.
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.
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.
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.
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 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 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
.
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:
.
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 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 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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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
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
.
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.
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.
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 }
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.
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.
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.
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.
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.
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.
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.
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.
18.222.118.90