- NSString *tableView:UITableView *theTableView titleForHeaderInSection:NSIntegersection { id theTitle = [sectionNames objectAtIndex:section]; if [theTitle isKindOfClass:[NSNull class]
Trang 1[NSArray arrayWithObjects:NSLocalizedString(@"Name", @"Name"), nil],
// Section 2
[NSArray arrayWithObjects:NSLocalizedString(@"Identity", @"Identity"), NSLocalizedString(@"Birthdate", @"Birthdate"),
NSLocalizedString(@"Sex", @"Sex"),
nil],
// Sentinel
nil];
rowKeys = [[NSArray alloc] initWithObjects:
// Section 1
[NSArray arrayWithObjects:@"name", nil],
id theTitle = [sectionNames objectAtIndex:section];
if ([theTitle isKindOfClass:[NSNull class]])
return nil;
return theTitle;
Trang 2NSString *rowKey = [rowKeys nestedObjectAtIndexPath:indexPath];
NSString *rowLabel = [rowLabels nestedObjectAtIndexPath:indexPath];
id <HeroValueDisplay, NSObject> rowValue = [hero valueForKey:rowKey];
cell.detailTextLabel.text = [rowValue heroValueDisplay];
Let’s take a look at the code we just wrote Notice first that we import both of the
categories we created earlier If we don’t import the category headers, the compiler
doesn’t know that those methods exist and will give us compile warnings We also
synthesize our only property:
Next comes viewDidLoad In this method, we create and populate those various arrays
we discussed earlier that will define the structure of our tables For now, we’re just going
to create the arrays here in code If our table gets more complex, we might want to
consider putting the contents of the arrays into property lists or text files and creating
the arrays from those files rather than hardcoding them as we’ve done here That would
reduce the size and complexity of our controller class At this point, there doesn’t seem
to be much benefit to doing that One of the nice things about this approach is that
since the arrays’ contents drive the table structure and the rest of the code in this
Trang 3controller class is relatively generic, we can change how we create our arrays without impacting the functionality of the rest of the code in this controller
The first array we populate is the sectionNames array Notice that because we are not using a property, we don’t have an accessor Since we’re not using an accessor that will retain the instance for us, we don’t release it After this line of code, sectionNames has a retain count of 1, which is exactly what it would be if we assigned it to a property specified with the retain keyword, and then released it after making the assignment
TIP: Notice that we pass a nil as the last parameter to initWithObjects: This is important
initWithObjects: is a variadic method, which is just a fancy way of saying it takes a
variable number of arguments We can pass in any number of objects to this method, and they will all get added to this array The terminating nil is how we tell the initWithObjects:
method that we’ve got not more objects for it This terminating nil is called a sentinel Starting with Snow Leopard, Xcode will warn you if you forget the sentinel, but on Leopard, a missing sentinel can be a very hard-to-debug problem
After this line of code fires, sectionNames has two elements The first one is that special placeholder, NSNull, we talked about If you look at Figure 4–2, you can see that the first section has no header This is how we’re going to indicate that there’s a section, but that it doesn’t have a header The second object in the array is a localized string that contains the word “General.” By creating a localized string, we have the ability to translate this header into whatever languages we wish If you need a refresher on
localizing your apps, the topic is covered in Chapter 17 of Beginning iPhone 3
Development
Next, we populate the rowLabels array This is the array that defines the blue labels displayed on each row that you can see in Figure 4–2 Notice again, that we’ve used localized strings so that if we want to later translate our labels into other languages, we have the ability to do so without having to change our code Because we’ve got nested object creation here, we’ve added comments so that when we revisit this somewhat complex code, we’ll remember what each bit of code is used for
rowLabels = [[NSArray alloc] initWithObjects:
// Section 1
[NSArray arrayWithObjects:NSLocalizedString(@"Name", @"Name"), nil],
// Section 2
[NSArray arrayWithObjects:NSLocalizedString(@"Identity", @"Identity"),
NSLocalizedString(@"Birthdate", @"Birthdate"),
NSLocalizedString(@"Sex", @"Sex"),
nil],
Trang 4// Sentinel
nil];
The code that populates the rowKeys array is very similar, except we don’t localize the
strings These are key values that are used to indicate which attribute gets shown in
which row, and localizing them would break the functionality The key is the same
regardless of the language our user understands
rowKeys = [[NSArray alloc] initWithObjects:
We have one more array, but we’re not populating it yet The last array defines which
controller classes are used to edit which rows We haven’t written any such controller
classes yet, so we’ve got nothing to put in that array We’re also not yet accessing this
array anywhere, so it’s okay to just put in a reminder to do it later As you’ve already
seen, when developing more complex applications, you will often have to implement
some functionality in an incomplete manner and then come back later to finish it
// TODO: Populate the rowControllers array
[super viewDidLoad];
}
The next method we implemented was dealloc, and there shouldn’t be anything too
surprising here We release all of the objects that we’ve retained, both those that are
associated with properties, and those that aren’t Remember, in viewDidLoad, we left our
various structure arrays at a retain count of 1, so we have to release them here to avoid
Even though we haven’t yet created or populated rowControllers, it’s perfectly okay to
release it here Sending a release message to nil is just fine and dandy in Objective-C
Next up are the table view datasource methods The first one we implement tells our
table view how many sections we have We return the count from sectionNames here By
doing that, if we change the number of objects in the sectionNames array, we
Trang 5automatically change the number of sections in the table and don’t have to touch this method
Since sections have an optional header displayed, we also implement
tableView:titleForHeaderInSection: For this, we just need to return the value from sectionNames If the value NSNull is stored as a section name, we need to convert it to nil, since that’s what UITableView expects for a section with no header
- (NSString *)tableView:(UITableView *)theTableView
titleForHeaderInSection:(NSInteger)section {
id theTitle = [sectionNames objectAtIndex:section];
if ([theTitle isKindOfClass:[NSNull class]])
return nil;
return theTitle;
}
In addition to telling our table view the number of sections, we need to tell it the number
of rows in each section Thanks to that category on NSArray we wrote earlier, this can be handled with one line of code It doesn’t matter which of the paired arrays we use, since they should all have the same number of rows in every subarray We obviously can’t use rowControllers, since we haven’t populated it yet We chose rowLabels, but rowKeys would have worked exactly the same
- (NSInteger)tableView:(UITableView *)theTableView
numberOfRowsInSection:(NSInteger)section {
return [rowLabels countOfNestedArray:section];
}
The tableView:cellForRowAtIndexPath: method is where we actually create the cell to
be displayed We start out almost exactly in the same way as every other table view controller, by looking for a dequeued cell and using it, or creating a new cell if there aren’t any dequeued cells
- (UITableViewCell *)tableView:(UITableView *)theTableView
Trang 6NSString *rowLabel = [rowLabels nestedObjectAtIndexPath:indexPath];
Once we know the attribute name, we can retrieve the object that’s used to represent
this attribute using valueForKey: Notice that we declare our rowValue object as id We
do this because the returned object could be instances of any number of different
classes We put HeroValueDisplay between angle brackets to indicate that we know the
returned object will be an object that conforms to that HeroValueDisplay protocol we
created earlier This gives us the ability to call the heroValueDisplay method on
whatever was returned without having to figure out what type of object it was
id <HeroValueDisplay, NSObject> rowValue = [hero valueForKey:rowKey];
Finally, we assign the label and value to the cell’s labels, and then return the cell
cell.detailTextLabel.text = [rowValue heroValueDisplay];
Using the New Controller
Now that we have our new controller class, we have to create instances of it somewhere
and push those onto the stack To do that, we have to revisit HeroListViewController
We could create a new instance of HeroEditController every time a row is tapped Only
one copy of HeroEditController will ever need to be on the navigation stack at a time
As a result, we can reuse a single instance over and over We can also save ourselves
several lines of code by adding an instance of HeroEditController to MainWindow.xib
and adding an outlet to that instance to HeroListViewController Remember, when you
add an icon to a nib, an instance of that object gets created when the nib loads
Declaring the Outlet
Single-click HeroListViewController.h, and add the following code to add an outlet for
the instance of HeroEditController we’re going to add to MainWindow.xib:
#import <UIKit/UIKit.h>
#define kSelectedTabDefaultsKey @"Selected Tab"
enum {
kByName = 0,
Trang 7kBySecretIdentity,
};
@class HeroEditController;
@interface HeroListViewController : UIViewController <UITableViewDelegate,
UITableViewDataSource, UITabBarDelegate, UIAlertViewDelegate,
@property (nonatomic, retain) IBOutlet UITableView *tableView;
@property (nonatomic, retain) IBOutlet UITabBar *tabBar;
@property (nonatomic, retain) IBOutlet HeroEditController *detailController;
@property (nonatomic, readonly) NSFetchedResultsController
Adding the Instance to MainWindow.xib
Double-click on MainWindow.xib to open the nib file in Interface Builder Look in the library for a Table View Controller, and drag one of those over to the nib’s main window
The newly added controller should be selected, so press 4 to bring up the identity
inspector and change the underlying class from UITableViewController to
HeroEditController
Next, in the main nib window, click on the Hero Edit Controller disclosure triangle and double-click on the Table View that appears Alternatively, you can just click in the Hero
Edit Controller window so the Table View shown in that window is selected Now, press
1 to bring up the attribute inspector You’ll know you’ve got the right item selected
when the inspector window’s title changes from Hero Edit Controller Attributes to Table
View Attributes Change the table’s Style from Plain to Grouped
Back in the main nib window, open the disclosure triangle to the left of Navigation
Controller to reveal an item named Hero List View Controller (Root View Controller)
Control-drag from that item to the Hero Edit Controller icon and select the
detailController outlet
Trang 8NOTE: Note that your Hero List View Controller (Root View Controller) might instead have the
name Hero List View Controller (SuperDB) No worries, it should work just fine
Save and close this nib and go back to Xcode
Pushing the New Instance onto the Stack
Single-click HeroListViewController.m There are two methods that we need to
implement When a user taps a row, we want to use the detail controller to show them
information about the hero on which they tapped When they add a new hero, we also
want to take them down to the newly added hero so they can edit it We haven’t
implemented the editing functionality yet, but we can still configure and push
detailController onto the stack now, so let’s do that
First, we need to import HeroEditController.h and synthesize the detailController
Now, find the addHero method, and add the following new code to it You can also
delete the old TODO comment
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
detailController.hero = newManagedObject;
[self.navigationController pushViewController:detailController animated:YES];
}
Trang 9We assign the new managed object to detailController’s hero property, which is how
we tell that controller that this is the hero to be viewed and/or edited Then, we push it onto the stack Easy enough?
Now, find tableView:didSelectRowAtIndexPath: It should just be a stub with a TODO comment Replace it with this new version:
corresponds to the row on which the user tapped
Trying Out the View Functionality
Save HeroListViewController.m and then build and run your application Try adding
new rows, or tapping on an existing row You still don’t have the ability to edit them, but when you add a new row, you should get a new screen of data that looks like
Figure 4–7
Figure 4–7 Adding a new hero now takes you to the new controller class
Trang 10All that’s missing is the ability to edit the individual fields, so let’s add that now
Adding Editing Subcontrollers
Our next step is to create a series of new controllers, each of which can be used to edit
an individual value on a hero For now, we need one that can edit string attributes
(Figure 4–8) and one that can edit date attributes (Figure 4–9) We’ll be adding other
controllers later All of these controllers have common functionality They’ll all take a
managed object and the name of the attribute on that managed object to be edited
They’ll all need a Save button and a Cancel button
Figure 4–8 The subcontroller that will allow the user to edit string attributes Here, it’s being used to edit the
name attribute
Trang 11Figure 4–9 The subcontroller that allows editing date attributes Here, it’s being used to edit the birthdate
attribute
Creating the Superclass
Whenever you are about to implement multiple objects that have some common
functionality, you should put some thought into whether that common functionality can
be put into a single class that the other controllers can then subclass In this case, there
is enough common functionality that a common superclass is appropriate Let’s create that common superclass now
Single-click the Classes folder in the Groups & Files pane and select New File… from the File
menu Create another UITableViewController subclass, as you did earlier when you created
the HeroEditController class Call this new class ManagedObjectAttributeEditor and make sure you create both the implementation and header file but do not create a nib file
Single-click ManagedObjectAttributeEditor.h, and replace the contents with this code:
Trang 12@property (nonatomic, retain) NSManagedObject *managedObject;
@property (nonatomic, retain) NSString *keypath;
@property (nonatomic, retain) NSString *labelString;
-(IBAction)cancel;
-(IBAction)save;
@end
TIP: Wondering about that funky looking arrow () at the end of the #define in the previous
chunk of code? That’s a continuation character Don’t type it! It just means that the current line
and the following line should be joined together as a single line
The constant kNonEditableTextColor is defined to match the color used in the table
view cell style UITableViewCellStyleValue2 We can’t use the default cell styles and let
the user edit values using a text field, but we want to match the appearance as closely
as we can (Figure 4–8)
We could have called the managedObject attribute hero instead, but by using more
generic terms, it’ll be easier to reuse this code in future projects Having a property
called hero wouldn’t make much sense if we were writing an application to keep track of
recipes, for example
Instead of attribute name, we’ve defined a property called keypath This will be the
attribute name, but by using keypath instead of key, we’ll have the ability to edit
attributes on other objects, not just on the one we’re editing Don’t worry if that doesn’t
make much sense now; you’ll see why we chose keypath instead of attribute or key in
Chapter 7 when we start talking about relationships and fetched properties We’ve also
provided a property for a label Not all subclasses will need this, but many will, so we’ll
provide the instance variable and property definition here in our superclass
We also define two methods, cancel and save, that will be called when the user presses
either of the buttons that will be presented Switch over to ManagedObjectAttributeEditor.m
and replace the existing contents with the following code:
Trang 13@"Save - for button to save changes")
// Objective-C has no support for abstract methods, so we're going
// to take matters into our own hands
NSException *ex = [NSException exceptionWithName:
@"Abstract Method Not Overridden"
reason:NSLocalizedString(@"You MUST override the save method",
@"You MUST override the save method")
Much of this should make sense to you, but there are a few things that warrant
explanation In the viewWillAppear: method, we are creating two bar button items to
go in the navigation bar You can see these two buttons, labeled Cancel and Save, in
Figure 4–8
Bar button items are similar to standard controls like UIButtons, but they are a special case, designed to be used on navigation bars and toolbars only One key difference between a bar button item and a regular UIButton is that bar button items only have one target and action They don’t recognize the concept of control events Bar button items
send their message on the equivalent of touch up inside only Here’s where we create the Cancel button The code that creates the Save button is nearly identical:
UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc]
Trang 14are several bar button styles, including one intended for Cancel buttons called
UIBarButtonSystemItemCancel, which we’ve used here
We also have to provide a target and action for the bar button item The target is self,
because we want it to call a method on the instance of this controller that is active The
action is a selector to one of those action methods we declared in the header file
Setting a target and action like this is exactly equivalent to control-dragging from a
button to a controller class and selecting an action method, we’re just doing it in code
this time because we don’t have a nib
The cancel method does nothing more than pop the subcontroller off the navigation
stack, which returns the user to the previous screen In this case, it will return them to
the detail view for the hero Since we don’t take any steps to capture the input from the
user, the managed object stays the same as it was before
NOTE: Strictly speaking, the save and cancel methods do not need to be declared with the
IBAction keyword, since we’re not triggering those methods from a nib They are, however,
action methods, and it is conceivable that at some point in the future, we could convert this
controller to using a nib file, so we declare both of the action methods with the IBAction
keyword just to be safe and to advertise that these are, indeed, methods that will be triggered by
user interface controls
The save method is a little unusual here We will never actually create an instance of this
class We’re creating this class only to contain common functionality that we expect to
exist among classes we’re going to write In most languages, we would define this as an
abstract class But Objective-C doesn’t have abstract classes, and it doesn’t have a
mechanism to force a subclass to implement a given method Therefore, just to be safe,
we throw an exception in our save method That way, if we ever forget to implement
save in a subclass we create, we’ll know about it instantly Instead of unpredictable
behavior, we’ll get slammed with a runtime exception While that may be a little
unpleasant when it happens, it will be very easy to debug because our exception will tell
us exactly what we did wrong
NSException *ex = [NSException exceptionWithName:
@"Abstract Method Not Overridden"
reason:NSLocalizedString(@"You MUST override the save method",
@"You MUST override the save method")
userInfo:nil];
[ex raise];
CAUTION: Objective-C does have exceptions, as you can see here Objective-C does not use
exceptions the way many other languages, such as Java and C++, do In Objective-C, exceptions
are used only for truly exceptional situations and are usually an indication of a problem within
your code They should never be used just to report a run-of-the-mill error condition Exceptions
are used with much less frequency in Objective-C then they are in many other languages
Trang 15Creating the String Attribute Editor
Now it’s time to create a generic controller class to handle the editing of string
attributes Single-click on Classes and create a new implementation and header file pair Just as you did before, create a subclass of UITableViewController and do not create a nib file Name the class ManagedObjectStringEditor Single-click
ManagedObjectStringEditor.h, and replace the contents with the following code:
functionality we implemented there, and we also define two constants that will be used
in a moment to let us retrieve subviews from the table view cell The default table view cell styles don’t allow in-place editing, so we have to customize the contents of our cell Since we don’t have a nib, we don’t have a way to connect outlets, so instead of using outlets, we’ll assign tags to each of the subviews we add to the table view cell, and then we’ll use that tag later to retrieve them
Save ManagedObjectStringEditor.h and switch over to ManagedObjectStringEditor.m
Replace the contents of that file with this code:
Trang 16NSEntityDescription *ed = [self.managedObject entity];
NSDictionary *properties = [ed propertiesByName];
NSAttributeDescription *ad = [properties objectForKey:self.keypath];
NSString *defaultValue = nil;
NSIndexPath *onlyRowPath = [NSIndexPath indexPathWithIndexes:onlyRow length:2];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:onlyRowPath];
UITextField *textField = (UITextField *)[cell.contentView
Trang 17Almost everything we do in this class is covered in Chapters 8 and 9 of Beginning
iPhone 3 Development, but there’s some code in tableView:cellForRowAtIndexPath:
that is worth taking a look at We’ve set default values for two of our attributes because they were required fields When the user taps one of those rows, they aren’t going to want to have to delete the default value before typing in the new value So, we’ve added some code to check to see if the current value is the same as the default value and, if it
is, we tell the text field to clear on editing
Here’s the code from tableView:cellForRowAtIndexPath: that does that First, we grab the current value held by the attribute
NSString *currentValue = [self.managedObject valueForKeyPath:self.keypath];
Next, we grab the managed object’s entity Information about an entity is returned in an NSEntityDescription instance:
NSEntityDescription *ed = [self.managedObject entity];
We can retrieve a dictionary with the properties, which includes attributes, by calling propertiesByName on the entity description
NSDictionary *properties = [ed propertiesByName];
We can retrieve the NSAttributeDescription that stores information about the attribute we’re editing from that dictionary using key-value coding:
NSAttributeDescription *ad = [properties objectForKey:self.keypath];
One piece of information that the attribute description holds is its default value, if any, so
we retrieve the default value
NSString *defaultValue = nil;
if (ad != nil)
defaultValue = [ad defaultValue];
Once we have the default value, we compare it to the current value If they’re not the same, then we set the text field’s value If they are the same, then we won’t bother populating the text field with the current value because we know they’re going to
change it
if (![currentValue isEqualToString:defaultValue])
textField.text = currentValue;
NOTE: Little details like not making your users spend time deleting default values can make the
difference between a good application and a great one Don’t expect to anticipate every possible detail in advance, however These are the kind of things that often don’t become obvious until you start testing and actually using the application, but when they become apparent, make sure you deal with them Annoying customers is not a good strategy
You should also notice that we implement the save method, overriding the one in our superclass, which throws an exception Looking at that save method, you might also be
Trang 18wondering if we made a mistake in this controller In Beginning iPhone 3 Development,
we warned against relying on controls on table view cells to maintain state for you, since
cells can get dequeued and reused to represent a different row Yet we are doing just
that here We are relying on a text field on a table view cell to keep track of the changes
the user has made to the attribute until they tap Save, at which point, we copy the value
from the text field back into the attribute In this particular case, we know that there will
always be exactly one row in this table Since a table view is always capable of
displaying one row, this cell can never get dequeued That makes this scenario an
exception to the general rule that you shouldn’t rely on table view cells to maintain state
for you
Creating the Date Attribute Editor
Create yet another table view subclass, this time calling the class
ManagedObjectDateEditor Once you’ve created the file, single-click on
ManagedObjectDateEditor.h and replace the contents with the following code:
@property (nonatomic, retain) UIDatePicker *datePicker;
@property (nonatomic, retain) UITableView *dateTableView;
- (IBAction)dateChanged;
@end
The controller for editing dates is slightly more complex than the one for editing a string
If you look at Figure 4–9, you’ll see that there is a text field that displays the current
value, and there is also a date picker that can be used to change the date
Save ManagedObjectDateEditor.h then single-click ManagedObjectDateEditor.m and
replace its contents with the following code:
Trang 19#pragma mark Table View Methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
}
Trang 20- (UITableViewCell *)tableView:(UITableView *)tableView
cell.textLabel.font = [UIFont systemFontOfSize:17.0];
cell.textLabel.textColor = [UIColor colorWithRed:0.243 green:0.306
Most of what’s going on in this class should be familiar to you The one thing that’s
somewhat strange with this is how we’ve implemented the date picker view If we had
just created a UIDatePicker and added it as a subview of our table view, then the picker
would have scrolled with the table and been unusable Instead, we use loadView, which
is used to create a user interface programmatically, and we create both a UIDatePicker
and a second UITableView We make both of these new objects subviews of our view
property This controller is actually modeled after the way that Apple’s Contacts
application accepts date inputs (Figure 4–10)
Trang 21Figure 4–10 When you add a date field to a person’s record in the Contacts application, this is the screen Our
date editing view controller recreates, pixel-for-pixel, this view
Using the Attribute Editors
There’s just one last task that we need to handle before we can try out our new iteration
of the SuperDB application We have to add code to use these new attribute editors
Single-click HeroEditController.m First, add the following declaration to the top of the
file:
#import "ManagedObjectAttributeEditor.h"
Next, in the viewDidLoad method, get rid of the TODO comment, and replace it with the code that follows This will define which controller class gets used for each row in each section
rowControllers = [[NSArray alloc] initWithObjects:
// Section 1
[NSArray arrayWithObject:@"ManagedObjectStringEditor"],
// Section 2
[NSArray arrayWithObjects:@"ManagedObjectStringEditor",
@"ManagedObjectDateEditor",
@"ManagedObjectStringEditor", nil],
// Sentinel
nil];
Trang 22Now, replace the tableView:didSelectRowAtIndexPath: method with the following:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *controllerClassName = [rowControllers
nestedObjectAtIndexPath:indexPath];
NSString *rowLabel = [rowLabels nestedObjectAtIndexPath:indexPath];
NSString *rowKey = [rowKeys nestedObjectAtIndexPath:indexPath];
Class controllerClass = NSClassFromString(controllerClassName);
This may be new to you, so let’s review it The first thing we do is retrieve the name of
the controller class that should be used to edit this particular row
NSString *controllerClassName = [rowControllers
nestedObjectAtIndexPath:indexPath];
We also retrieve the attribute name and label for the selected row
NSString *rowLabel = [rowLabels nestedObjectAtIndexPath:indexPath];
NSString *rowKey = [rowKeys nestedObjectAtIndexPath:indexPath];
Next, we use a special function called NSClassFromString() that creates an instance of a
class based on its name stored in an NSString instance
Class controllerClass = NSClassFromString(controllerClassName);
After this line of code, controllerClass will be the class object for the class whose name
we put in the rowController array You can use a Class object just like you can the
name of the class when you alloc a new object So, if controllerClassName was Foo,
then doing
id theObject = [controllerClass alloc];
would be exactly the same as calling
id theObject = [foo alloc];
So, in the next line of code, we do this:
ManagedObjectAttributeEditor *controller =
[controllerClass alloc];
Here, we’re actually creating an instance of the class that will be used to edit this
particular attribute That’s probably a little confusing and, if so, don’t worry too much It
can take some time to get used to Objective-C’s dynamic nature We’ve already
Trang 23allocated the controller Now, we just need to initialize it, set its properties, then push it onto the navigation stack, like so:
controller = [controller initWithStyle:UITableViewStyleGrouped];
Save HeroEditController.m and build and run your application You should be able to
edit all the attributes by tapping a row
Implementing a Selection List
There’s one last loose end to take care of This version of our application uses the string attribute editor to solicit the sex (sorry, we couldn’t resist!) of the superhero This means that there is no validation on the input other than that it’s a valid string A user could
type M, Male, MALE, or Yes, Please, and they would all be happily accepted by the
string attribute editor That means, later on, if we want to let the user sort or search their heroes by gender, we could have problems, because the data won’t be structured in a consistent manner
As you saw earlier, we could have enforced a specific sex spelling by using a regular
expression, putting up an alert if the user typed something besides Male or Female This
would have prevented values other than the ones we want from getting entered, but this approach is not all that user friendly We don’t want to annoy our user Why make them type anything at all? There are only two possible choices here Why not present a selection list and let the user just tap the one they want? Hey, that sounds like a great idea! We’re glad you thought of it Let’s implement it now, shall we?
We could, of course, write a special controller to present a two-item list, but that
wouldn’t be the best use of our time Such a controller would only be useful when we were soliciting sex (gee, did we do that again?) Wouldn’t it be more useful to create a controller that can be used for any selection list? Of course it would, so let’s do that
Creating the Generic Selection List Controller
Create a new table view controller as you did previously, calling this class
ManagedObjectSingleSelectionListEditor After you create the files, single-click on ManagedObjectSingleSelectionListEditor.h and replace its contents with the following
Trang 24The structure here might seem somewhat familiar It’s almost identical to one of the
controllers from the Nav application in Chapter 9 of Beginning iPhone 3 Development
The list property will contain the array of values from which the user can select, and
lastIndexPath will be used to keep track of the selection
Save ManagedObjectSingleSelectionListEditor.h and single-click on
ManagedObjectSingleSelectionListEditor.m Replace the contents of that file with the
NSString *newValue = selectedCell.textLabel.text;
[self.managedObject setValue:newValue forKey:self.keypath];
NSString *currentValue = [self.managedObject valueForKey:self.keypath];
for (NSString *oneItem in list) {
if ([oneItem isEqualToString:currentValue]) {
NSUInteger newIndex[] = {0, [list indexOfObject:oneItem]};
NSIndexPath *newPath = [[NSIndexPath alloc] initWithIndexes:
Trang 25- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [list count];
}
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
int newRow = [indexPath row];
int oldRow = [lastIndexPath row];
if (newRow != oldRow || newRow == 0) {
UITableViewCell *newCell = [tableView cellForRowAtIndexPath:indexPath];
NSUInteger row = [indexPath row];
NSUInteger oldRow = [lastIndexPath row];
cell.textLabel.text = [list objectAtIndex:row];
cell.accessoryType = (row == oldRow && lastIndexPath != nil) ?
UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
return cell;
}
@end
There’s really nothing new here The logic we’re using is exactly the same that we used
in the Nav application If you aren’t sure what’s going on here, go back and take a look through Chapter 9 of Beginning iPhone 3 Development The only difference here is that
we’re using the keypath and managedObject to determine the initial selection and then
pushing the final selection back into managedObject when the user presses the Save
button
Now, the question is, how do we provide the values (Male and Female) to this
subcontroller? Remember: we want to avoid creating special cases We want to keep our code as generic as possible We don’t want to, for example, hard code a check for this new controller’s class, and then set the list property That would work, but we want
to find a solution that’s flexible, reusable, and easy to maintain as our application grows
Trang 26What we’re going to do is create another paired nested array to hold additional
arguments to be passed on to the subordinate controller Anything we put into this
dictionary for a given row will be passed along to the subordinate controller using
key-value coding This gives us the flexibility to pass on any information to any controller we
create
The first step toward implementing this is to add an instance variable for the new nested
array Single-click HeroEditController.h and add the following line of code:
Save HeroEditController.h and flip over to HeroEditController.m We need to make two
changes here First, we need to create and populate the new rowArguments array And
second, we need to write code to pass the key/value pairs from that array on to the
subordinate controller
First, look for the viewDidLoad method Find where we create and populate
rowControllers, and replace that code with the following version, which changes the
controller used for the row that represents the hero’s sex
rowControllers = [[NSArray alloc] initWithObjects:
// Section 1
[NSArray arrayWithObject:@"ManagedObjectStringEditor"],
// Section 2
[NSArray arrayWithObjects:@"ManagedObjectStringEditor",
@"ManagedObjectDateEditor",
@"ManagedObjectSingleSelectionListEditor", nil],
// Sentinel
Trang 27Now, find tableView:didSelectRowAtIndexPath: and insert the following code, which retrieves the arguments for this row and, if the object retrieved is a dictionary, it loops through the keys contained in that dictionary and passes the key and value on to the controller
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *controllerClassName = [rowControllers
nestedObjectAtIndexPath:indexPath];
NSString *rowLabel = [rowLabels nestedObjectAtIndexPath:indexPath];
NSString *rowKey = [rowKeys nestedObjectAtIndexPath:indexPath];
Class controllerClass = NSClassFromString(controllerClassName);
NSDictionary *args = [rowArguments nestedObjectAtIndexPath:indexPath];
if ([args isKindOfClass:[NSDictionary class]]) {
if (args != nil) {
for (NSString *oneKey in args) {
id oneArg = [args objectForKey:oneKey];
[controller setValue:oneArg forKey:oneKey];
Trang 28TIP: The isKindOfClass: method that we used in this new chunk of code is a method that
will return YES when called on an instance of a specific class or an instance of any class that
descends from that class In this case, since we pass the NSArray class object in as an
argument, the method will return YES if args was an instance of NSDictionary or if it was an
instance of NSMutableDictionary, but would return NO if args is the singleton NSNull
Save HeroEditController.m and build and run the application This time, when you tap on
the row labeled Sex, you should get a nice, user-friendly list like in Figure 4–11
Figure 4–11 The selection list controller being used to present two options for the sex attribute
Devil’s End
Well, we’re at the end of a long and conceptually difficult chapter You should
congratulate yourself on making it all the way through with us Table-based detail editing
view controllers are some of the hardest controller classes to write well, but now you
have a handful of tools in your toolbox to help you create them You’ve seen how to use
nested and paired arrays to define your table view’s structure, you’ve seen how to
create generic classes that can be used to edit multiple types of data, and you’ve also
seen how to use Objective-C’s dynamic nature to create instances of classes based on
the name of the class stored in an NSString instance
Ready to move on? Turn the page Let’s get going!