Wednesday, February 25, 2009

Editable Select List

For a project I was working on, we had a text field that the users would tend to enter the same handful of values over and over. In fact, the fact that they had to keep entering the same values over and over was quite frustrating to our testers. But we couldn't provide a set list, because it wouldn't be the same values for all users. They needed the flexibility to add any value they needed, but wanted the convenience to not have to enter ones that they had already entered. On a desktop app, the likely answer would have been a combo-box, or a text field with type-ahead that would allow the user to type only a few characters of the value and then hit tab or return to select it.

The iPhone doesn't have combo-boxes, and type-ahead would be a pain to implement and I had concerns that it might get dinged in the review process (Yes, Apple, your review policies are definitely having a chilling effect). The answer I came up with for handling this situation was to create an editable selection list controller. It works just like the Generic Selection List Controller I posted about a week ago, except that it tacks one item onto the end of the table to allow the user to add a new item to the list:


When you select that last item, it uses the Multiple Text Field Editing Controller to prompt the user for the new value:



At some point, I'd like to refactor this class, and the SelectionListViewcontroller into one class, as there is a lot of common ground between them, but for now, it's a separate class. You must have the TextFieldEditingViewcontroller class in your project also, because it uses that to let the user enter new values.

EditableSelectionListViewController.h
//
// SelectionListViewController.h
//
// Created by Jeff LaMarche on 2/18/09.
// Copyright 2009 Jeff LaMarche Consulting. All rights reserved.
//

#import <UIKit/UIKit.h>
#import "AbstractGenericViewController.h"
#import "TextFieldEditingViewController.h"

@protocol EditableSelectionListViewControllerDelegate <NSObject>
@required
- (void)rowChosen:(NSInteger)row fromArray:(NSMutableArray *)theList;
@end


@interface EditableSelectionListViewController : AbstractGenericViewController <TextFieldEditingViewControllerDelegate>
{
NSMutableArray *list;
NSIndexPath *lastIndexPath;
NSInteger initialSelection;

id <EditableSelectionListViewControllerDelegate> delegate;
}

@property (nonatomic, retain) NSIndexPath *lastIndexPath;
@property (nonatomic, retain) NSArray *list;
@property NSInteger initialSelection;
@property (nonatomic, assign) id <EditableSelectionListViewControllerDelegate> delegate;
@end



EditableSelectionListViewController.m
//
// SelectionListViewController.m
//
// Created by Jeff LaMarche on 2/18/09.
// Copyright 2009 Jeff LaMarche Consulting. All rights reserved.
//

#import "EditableSelectionListViewController.h"


@implementation EditableSelectionListViewController
@synthesize list;
@synthesize lastIndexPath;
@synthesize initialSelection;
@synthesize delegate;
-(IBAction)save
{
[self.delegate rowChosen:[lastIndexPath row] fromArray:list];
[self.navigationController popViewControllerAnimated:YES];
}

#pragma mark -
- (id)initWithStyle:(UITableViewStyle)style
{
initialSelection = -1;
return self;
}

- (void)viewWillAppear:(BOOL)animated
{
// Check to see if user has indicated a row to be selected, and set it
if (initialSelection > - 1 && initialSelection < [list count])
{
NSUInteger newIndex[] = {0, initialSelection};
NSIndexPath *newPath = [[NSIndexPath alloc] initWithIndexes:newIndex length:2];
self.lastIndexPath = newPath;
[newPath release];
}


[super viewWillAppear:animated];
}

- (void)dealloc
{
[list release];
[lastIndexPath release];
[super dealloc];
}

#pragma mark -
#pragma mark Tableview methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [list count] + 1;
}

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

static NSString *SelectionListCellIdentifier = @"SelectionListCellIdentifier";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SelectionListCellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:SelectionListCellIdentifier] autorelease];
}


NSUInteger row = [indexPath row];
NSUInteger oldRow = [lastIndexPath row];
if (row >= [list count])
{
cell.font = [UIFont boldSystemFontOfSize:19.0];
cell.text = NSLocalizedString(@"Other…", @"Other…");
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}

else
{
cell.font = [UIFont systemFontOfSize:19.0];
cell.text = [list objectAtIndex:row];
cell.accessoryType = (row == oldRow && lastIndexPath != nil) ? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;

}


return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
int newRow = [indexPath row];
int oldRow = [lastIndexPath row];

if (newRow < [list count])
{
if (newRow != oldRow)
{
UITableViewCell *newCell = [tableView cellForRowAtIndexPath:indexPath];
newCell.accessoryType = UITableViewCellAccessoryCheckmark;

UITableViewCell *oldCell = [tableView cellForRowAtIndexPath: lastIndexPath];
oldCell.accessoryType = UITableViewCellAccessoryNone;

lastIndexPath = indexPath;
}

}

else
{
TextFieldEditingViewController *controller = [[TextFieldEditingViewController alloc] initWithStyle:UITableViewStyleGrouped];
controller.fieldKeys = [NSArray arrayWithObject:@"newValue"];
controller.fieldNames = [NSArray arrayWithObject:NSLocalizedString(@"New Item", @"New Item")];
controller.fieldValues = [NSArray arrayWithObject:@""];
controller.delegate = self;
[self.navigationController pushViewController:controller animated:YES];
}

[tableView deselectRowAtIndexPath:indexPath animated:YES];
}

#pragma mark -
- (void)selectRow:(NSIndexPath *)theIndexPath
{
//[self.tableView selectRowAtIndexPath:theIndexPath animated:YES scrollPosition:UITableViewScrollPositionBottom];
[self tableView:self.tableView didSelectRowAtIndexPath:theIndexPath];
}

- (void)valuesDidChange:(NSDictionary *)newValues
{
NSString *newVal = [newValues objectForKey:@"newValue"];
[list addObject:newVal];
//[self.tableView reloadData];

[list sortUsingSelector:@selector(compare:)];
NSUInteger theIndices[] = {0, [list indexOfObject:newVal]};
NSIndexPath *theIndexPath = [[NSIndexPath alloc] initWithIndexes:theIndices length:2];
[self performSelector:@selector(selectRow:) withObject:theIndexPath afterDelay:0.05];
// [self tableView:self.tableView didSelectRowAtIndexPath:theIndexPath];
[self.tableView reloadData];
}

@end






11 comments:

SethOnStuff said...

Is there a way to get Firefox to print these blog entries? I always get 3 pages, with only the first having part of the posting.

Jeff LaMarche said...

Seth - honestly, I don't know. I don't use Firefox very often :(

vibhor goyal said...

Where do I get the code for :

AbstractGenericViewController.h.

Thanks

Jules R said...

There are a few bugs with this code, trying it on iPhone 3.1.2

Both are in the EditableSelectionListViewController.m

(1) In valuesDidChange:

If the line "[self performSelector:..." line is not taken out, then the navigation controller buttons are messed and SAVE stays linked to the TextFieldEditingViewController.save

(2) The "list" variable is not initialized properly, so valuesDidChange does not affect the items.

To fix this, add
-(void)viewDidLoad {
list = [[NSMutableArray alloc] init];
}

Jules R said...

@vibhor: the code for this site is in http://code.google.com/p/iphonebits/

Christian Rowe said...

Can you make a zip folder with this code please :)

oldbeamer said...

Jeff, I believe that in didSelectRowAtIndexPath the code:
lastIndexPath = indexPath;
should be
self.lastIndexPath = indexPath;

Edwin said...

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

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

T said...

Hi, thanks for the code example. I came across this as I was trying to find a way of ensuring my selected indexpath.row = primarykey for sqlite db update. Will this code help with that?! Thanks again!