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






18 comments:

Berko said...

You're awesome

Seventoes said...

Beautiful! <3 this blog so much.

KAPS said...

Thanks a lot, I've been looking for this simple solution for too long. Is indeed strange that Apple doesn't provide a standard way to display these kind of cells

arielm said...

OpenGL and now UI matters? The perfect menu indeed! So thank you for the great work and keep on rocking!

kinoli said...

This is a great reusable class. I'm not sure exactly how to implement it though. I have a view controller class that I'm trying to add it to inside a view with this code... [formView addSubview:controller];

It doesn't seem to work properly though. Also, I'm getting a warning

"class 'ShareViewController' does not implement the 'TextFieldEditingViewControllerDelegate' protocol"

Any ideas?

Thanks a lot for providing this class. I'm just too much of a noob to know how to use it.

JES said...

Hey, this is neat. I just wrote something similar. In mine, I distinguish between entering new data versus editing old data. The difference is that the new data will display placeholders in the text fields. I can also display field labels, but they are optional.

About the 5 field max: I didn't put any kind of limitation because I thought tables can scroll?

You can look at my version here.

Sombra said...

Excellent post.
I think you miss in tableView:cellForRowAtIndexPath: release the textField after add it to the cell's contentView.
I copy your code and customize a bit. works fine. Thanks!

One question for next post. How center programtaically a textField in height? Notice if you change a font size it looks uncentered.

Sorry form my english.

hawkwood said...

Any suggestions on how to get the first row to becomeFirstResponder when the view is pushed in?

Alex said...

I May be mistaken but, isn't there a memory leak when you assign the changedValues array?

changedValues = [theFieldValues mutableCopy];

changedValues is not being released in the dealloc method...

Paul Westbrook said...

It would be great to be able to use this, and if the delegate returned a nil UINavigationController, this controller would create it's own UINaviagationBar.

Jeremy said...

it would probably be best to add
if ([indexPath row] == 0)
{
[textField becomeFirstResponder];
}
before return cell; in cellForRowAtIndexPath, this will save the users having to click on the first field to see the keyboard.

raf said...

This is really great! Thank you.

Jeremy said...

I can confirm with Alex's post on June 22, 2009 3:36 PM , There is a memory lead that shows in Instruments, by adding [changedValues release]; to dealloc it fixes the problem.

Joe said...

This is full of awesome. Thanks!

The only thing I ran into is a missing release on newPath within textFieldDone: (right after assigning nextCell). Minor nit though. Again, big-time thanks for sharing this!

Edwin said...

scrub m65 kamagra attorney lawyer body scrub field jacket lovegra marijuana attorney injury lawyer

jack chan said...

Louis Vuitton Aviator M40385
Louis Vuitton Aviator M40384
Louis Vuitton Aviator M40383
Louis Vuitton Mahina leather Stellar PM M93177
Louis Vuitton Mahina leather Stellar PM M93176
Louis Vuitton Mahina leather Stellar PM M93175
taiga leather
bum bag
louis vuitton totally
louis vuitton denim
Pochette
Stephen Sprouse
louis vuitton stephen sprouse
Monogram Vernis
LV Monogram Vernis

JeansPilot said...

JeansPilot offers the chance to buy a large variety of men’s and women’s jeans clothing from the world famous Italian Brands.
Online jeans clothing store looks for original fashion clothing sales and clearances of worldwide known designers. We participate in fashion auctions to get the lowest possible price for Top quality Clothes, Shoes and Accessories.
Buy Jeans

h4ns said...

What youre saying is completely true. I know that everybody must say the same thing, but I just think that you put it in a way that everyone can understand. I also love the images you put in here. They fit so well with what youre trying to say. Im sure youll reach so many people with what youve got to say.

Arsenal vs Huddersfield Town live streaming
Arsenal vs Huddersfield Town live streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Notts County vs Manchester City Live Streaming
Notts County vs Manchester City Live Streaming
Bologna vs AS Roma Live Streaming
Bologna vs AS Roma Live Streaming
Juventus vs Udinese Live Streaming
Juventus vs Udinese Live Streaming
Napoli vs Sampdoria Live Streaming
Napoli vs Sampdoria Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
AS Monaco vs Marseille Live Streaming
AS Monaco vs Marseille Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Technology News | News Today | Live Streaming TV Channels