Sunday, December 28, 2008

Reusable Controllers

I thought I'd take a break from the OpenGL stuff to finish a post I've been thinking about doing for a while. One misconception that a lot of newer Cocoa/Cocoa Touch developers seem to have is that your application controller classes have to be completely custom. There is some basis for this belief, as every application will need to have some functionality that is specific to itself, but there are a number of generic, reusable controller classes, such as UITableViewController and UINavigationController that handle some or all aspects involved with controlling a certain type of user interface.

Yet, a lot of people don't carry this idea of reusability into their own controller code for some reason. All too often you see projects with several controller classes that are all very similar to each other. One place I've seen this a lot is when people implement table-based detail editing views like the ones used in the Contacts application. Here is an example of what I'm talking about:

I'm honestly surprised that Apple doesn't provide a generic controller class for this particular scenario, but they don't. There's no reason why we can't create our own generic editable detail pane controller class.

I'm going to provide the code for a version that I've used in a few projects. It allows the editing of up to five text fields. The reason only five fields is supported is because that's all that can be displayed along with the keyboard. If you need more than five fields, you should probably split it into multiple drill downs.

To use this class, you create the controller and set yourself as the delegate. You then set three properties. All three arrays represent the fields to be displayed, so the item at index 0 in all three arrays is different data about the same field, like so:
TextFieldEditingViewController *controller = [[TextFieldEditingViewController alloc] initWithStyle:UITableViewStyleGrouped];
controller.delegate = self;

controller.fieldNames = myArrayContainingTheLabelsToBeDisplayed;
controller.fieldKeys = myArrayContainingTheKeyValuesForTheItemToBeDisplayed;
controller.fieldValues = myArrayContainingTheCurrentValuesForTheItemsToBeDisplayed;

fieldNames should contain the label to be used by the detail controller for each item. This is the text in blue in the screenshot above that the user sees. The fieldKeys is the key that corresponds to each field. This will generally be the property name for the field being edited, and will be used in the method that is used to pass values back to the delegate. fieldValues will hold the current values for each of the fields. All threes of these properties must be set, and they must be NSArrays or a subclass of it. All three must contain the same number of objects, and all three must contain five objects or less.

There's another optional property that you can set: keyboardTypes, which is a five element array of UIKeyboardTypes. This can be used to tell the detail controller what type of keyboard to show the user for each field that can be edited. These all default to UIKeyboardTypeAlphabet, but if, say, one of the values you want to edit is phone number, you can pass UIKeyboardTypePhonePad for the array index that corresponds to that field.

Notice in the pseudo-code above that we set self as the controller's delegate. This is how we will be notified if the user edited any of the fields. The valuesDidChange: method will be called on the delegate, passing a dictionary using key-value coding that informs the delegate of all the changes made if the user pressed the Save button. The delegate is then responsible for taking the values from that array and stuffing them in the appropriate place.

This class is not as polished and hasn't been as thoroughly tested as Apple's generic controller classes, but it sure has reduced the number of controller classes I need in some of my table-based applications. So, without further ado, here she is:

TextFieldEditingViewController.h
/*
TextFieldEditingViewController.h
*/


#import <UIKit/UIKit.h>
#define kDefaultLabelTag 50002

@protocol TextFieldEditingViewControllerDelegate <NSObject>
@required
- (void)valuesDidChange:(NSDictionary *)newValues;
- (UINavigationController *)navController; // Return the navigation controller
@end


@interface TextFieldEditingViewController : UITableViewController <UITextFieldDelegate> {

NSArray *fieldNames; // Field name to be displayed to user
NSArray *fieldKeys; // Key value to be used in dictionary when values are passed back to delegate
NSArray *fieldValues; // Starting display values for each field. Values should be strings.

NSMutableArray *changedValues; // Changes will be stored in this array , which will also be passed back to delegate on save
UIKeyboardType keyboardTypes[5]; // Keyboard types for each field

id <TextFieldEditingViewControllerDelegate> delegate; // Delegate who will received the changed values. Delegate
// is responsble for converting back from string if necessary

UITextField *textFieldBeingEdited; // The field currently being edited

UIBarButtonItem *oldLeftButton; // Used to restore old button values
UIBarButtonItem *oldRightButton;
}

@property (nonatomic, retain) NSArray *fieldNames;
@property (nonatomic, retain) NSArray *fieldKeys;
@property (nonatomic, retain) NSArray *fieldValues;
@property (nonatomic, retain) NSMutableArray *changedValues;
@property (nonatomic, assign /* for weak ref */) id <TextFieldEditingViewControllerDelegate> delegate;
@property (nonatomic, retain) UITextField *textFieldBeingEdited;
@property (nonatomic, retain) UIBarButtonItem *oldLeftButton;
@property (nonatomic, retain) UIBarButtonItem *oldRightButton;
-(IBAction)cancel;
-(IBAction)save;
-(IBAction)textFieldDone:(id)sender;
-(void)setKeyboardType:(UIKeyboardType)theType forIndex:(NSUInteger)index;
@end




TextFieldEditingViewController.m
/*
TextFieldEditingViewController.m

*/

#import "TextFieldEditingViewController.h"
#import "BirthdaysAppDelegate.h"

@implementation TextFieldEditingViewController
@synthesize fieldNames;
@synthesize fieldKeys;
@synthesize fieldValues;
@synthesize delegate;
@synthesize changedValues;
@synthesize textFieldBeingEdited;
@synthesize oldLeftButton;
@synthesize oldRightButton;
- (id)initWithStyle:(UITableViewStyle)style
{
if (self = [super initWithStyle:style])
{
for (int i =0; i < 5; i++)
keyboardTypes[i] = UIKeyboardTypeAlphabet;
}
return self;
}
-(IBAction)textFieldDone:(id)sender
{
UITableViewCell *cell = (UITableViewCell *)[[(UIView *)sender superview] superview];
UITableView *table = (UITableView *)[cell superview];
NSIndexPath *textFieldIndexPath = [table indexPathForCell:cell];
NSUInteger row = [textFieldIndexPath row];
row++;
if (row >= [fieldNames count])
row = 0;
NSUInteger newIndex[] = {0, row};
NSIndexPath *newPath = [[NSIndexPath alloc] initWithIndexes:newIndex length:2];
UITableViewCell *nextCell = [self.tableView cellForRowAtIndexPath:newPath];
UITextField *nextField = nil;
for (UIView *oneView in nextCell.contentView.subviews)
{
if ([oneView isMemberOfClass:[UITextField class]])
nextField = (UITextField *)oneView;
}
[nextField becomeFirstResponder];

}
- (void)viewWillAppear:(BOOL)animated
{
self.oldLeftButton = self.navigationItem.leftBarButtonItem;
self.oldRightButton = self.navigationItem.rightBarButtonItem;

UIBarButtonItem *saveButton = [[UIBarButtonItem alloc]
initWithTitle:@"Save"
style:UIBarButtonItemStylePlain
target:self
action:@selector(save)]
;
self.navigationItem.rightBarButtonItem = saveButton;
[saveButton release];

UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc]
initWithTitle:@"Cancel"
style:UIBarButtonItemStylePlain
target:self
action:@selector(cancel)]
;
self.navigationItem.leftBarButtonItem = cancelButton;
[cancelButton release];

[super viewWillAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
self.navigationItem.leftBarButtonItem = oldLeftButton;
self.navigationItem.rightBarButtonItem = oldRightButton;
[super viewWillDisappear:animated];
}
-(IBAction)cancel
{
[[self.delegate navController] popViewControllerAnimated:YES];
}
-(IBAction)save
{
if (textFieldBeingEdited != nil)
[changedValues replaceObjectAtIndex:textFieldBeingEdited.tag withObject:textFieldBeingEdited.text];

[self.delegate valuesDidChange:[NSMutableDictionary dictionaryWithObjects:changedValues forKeys:fieldKeys]];
[[self.delegate navController] popViewControllerAnimated:YES];
}
-(void)setKeyboardType:(UIKeyboardType)theType forIndex:(NSUInteger)index
{
keyboardTypes[index] = theType;
}
#pragma mark -
- (void)setFieldNames:(NSArray *)theFieldNames
{
if ([theFieldNames count] > 5)
{
NSException *e = [NSException exceptionWithName:@"Too Many Values"
reason:@"If more than five values are provided, some will be inaccessible because of the keyboard view"
userInfo:nil
]
;
[e raise];
}
[theFieldNames retain];
[fieldNames release];
fieldNames = theFieldNames;
}
- (void)setFieldValues:(NSArray *)theFieldValues
{
[theFieldValues retain];
[fieldValues release];
fieldValues = theFieldValues;

changedValues = [theFieldValues mutableCopy];
}




- (void)dealloc {
[fieldNames release];
[fieldKeys release];
[fieldValues release];
[textFieldBeingEdited release];
[super dealloc];
}

#pragma mark -
#pragma mark Table View Data Source Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [fieldNames count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *textFieldCellIdentifier = @"textFieldCellIdentifier";


UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:textFieldCellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:textFieldCellIdentifier] autorelease];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(10, 10, 75, 25)];
label.textAlignment = UITextAlignmentRight;
label.tag = kDefaultLabelTag;
UIFont *font = [UIFont boldSystemFontOfSize:11];
label.textColor = [UIColor colorWithRed:0.4 green:0.4 blue:0.6 alpha:1.0];
label.font = font;
[cell.contentView addSubview:label];
[label release];


UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(94, 10, 200, 25)];
//textField.tag = kDefaultTextFieldTag;
textField.clearsOnBeginEditing = NO;
[textField setDelegate:self];
[textField addTarget:self
action:@selector(textFieldDone:)
forControlEvents:UIControlEventEditingDidEndOnExit
]
;
[cell.contentView addSubview:textField];
}
UILabel *label = (UILabel *)[cell.contentView viewWithTag:kDefaultLabelTag];
UITextField *textField = nil;
for (UIView *oneView in cell.contentView.subviews)
{
if ([oneView isMemberOfClass:[UITextField class]])
textField = (UITextField *)oneView;
}

label.text = [fieldNames objectAtIndex:[indexPath row]];
textField.text = [changedValues objectAtIndex:[indexPath row]];
textField.tag = [indexPath row];
textField.keyboardType = keyboardTypes[[indexPath row]];
return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}

#pragma mark -
#pragma mark Text Field Delegate Methods
#pragma mark -
#pragma mark Table Delegate Methods
- (NSIndexPath *)tableView:(UITableView *)tableView
willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
return nil;
}
#pragma mark -
#pragma mark Text Field Delegate Methods
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
self.textFieldBeingEdited = textField;
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{

[changedValues replaceObjectAtIndex:textField.tag withObject:textField.text];
}

@end


No comments:

Post a Comment