Saturday, February 28, 2009

360 iDev Conference, T-Minus 18 hours

It's just about eighteen hours until I leave my house for the 360 iDev conference in San Jose. I'm excited about the trip despite the fact that I'm completely snowed under and behind on my work thanks to a few days of feeling lousy (combined with most of my family and the dog also feeling lousy). Fortunately for me, there's either a loophole in Northwest Airline's elite policy or a bug in their ticketing system.

Since I started the iPhone book almost a year ago, I haven't been traveling very much. Before that, I traveled constantly as part of my consulting work, so I used to have the highest level of status on two airlines and in two hotel chains. That is all gone now. Starting with 2009, all my status is either gone, or reduced to Silver level.

It's a good trade-off. I'd rather be home more with my family more and working with technologies I believe in, but when I do travel, I am going to miss the various perks of status, especially those first class upgrades on long cross-country flights.

But, as it turns out, I won't be missing them on this trip. I still had my status with Northwest when I made the reservations for the conference, and as far as the ticketing system is concerned for upgrade purposes, I still have that status, so it upgraded me for all three flights tomorrow (yes, three flights - urgh). Which means I'll actually be able to get work done on the plane as long as my battery holds out. Of course, now I'm really regretting not pre-ordering a new 17" MBP as I had intended, with its claimed eight-hour battery life.

Anyway, if you're planning on going to the conference, say "Hi" if you see me. If you're not planning to go but you're a reasonable distance from the San Jose, I believe there are still tickets available and it looks like it's going to be a great couple of days.

Friday, February 27, 2009

Mapping Directions from your App

If you want to give your user driving directions by launching the Maps application, it's actually pretty easy. You just create a regular Google Maps URL and use UIApplication to open it up; Your phone will recognize that the URL is for directions and will open Maps.app rather than Safari.app automatically.

The URL format is very easy for directions:

http://maps.google.com/maps?saddr=[source address or coordinates]&daddr=[destination address or coordinates]

Here is an example of taking the coordinates pulled from Core Location, and using that as the starting point for directions:

    NSString *destinationString = @"Cupertino,California";
NSString *url = [NSString stringWithFormat: @"http://maps.google.com/maps?saddr=%f,%f&daddr=%@", newLocation.coordinate.latitude, newLocation.coordinate.longitude, destinationString];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]];

Of course, this doing this quits your application and launches Maps.app, so make sure you've got everything saved before you do it.

There is one gotcha if you're using search terms or a physical address instead of coordinates. Maps.app doesn't like spaces. Unlike Safari, Maps.app won't automatically convert spaces to %20 for you. On the other hand, you don't want to use NSString's stringByAddingPercentEscapesUsingEncoding: either, because that method will convert commas, dashes, and certain other characters, which may cause problems when launching Maps.app (and probably will - those commas are important!).

In most cases, you can just remove the spaces by doing this:

    NSString *newAddressPartOfURL = [addressPartOfURL stringByReplacingOccurrencesOfString:@" " withString:@""]

Technically speaking, we're cheating. We should be able to URL-encode the whole address no problem, and it should work. But… it doesn't. This is one of those situations where you need to cheat a little to get things to work properly. I can tell you from first-hand experience that if you URL-encode those commas in your address, you will get a message that Maps.app couldn't find the specified address.

Reusable Code in Google Code

As per a reader request, I have created a repository with all of the re-usable code that I've posted here in my blog. You can find the repository over at Google Code.

I'll try and keep this updated with bug fixes and also add any new reusable code to the repository.

Thursday, February 26, 2009

Alert View with Prompt

Here's another generic class for you. This one doesn't require a navigation app - it can be used pretty much anywhere. It's a custom-subclass of UIAlertView that lets the user type in a value. It supports only two buttons - Okay and Cancel - but it handles everything to do with the text field for you. It looks like this:


You use it pretty much the same way you use an alert view. You allocate and initialize it, call show and then release it:

    AlertPrompt *prompt = [AlertPrompt alloc];
prompt = [prompt initWithTitle:@"Test Prompt" message:@"Please enter some text in" delegate:self cancelButtonTitle:@"Cancel" okButtonTitle:@"Okay"];
[prompt show];
[prompt release];

Then, you implement the appropriate UIAlertView callback method, and grab the entered text from the alert view instance. You have to cast the alert view back to an AlertPrompt instance, but other than that, everything is the same as using a standard UIAlertview:

- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex
{
if (buttonIndex != [alertView cancelButtonIndex])
{
NSString *entered = [(AlertPrompt *)alertView enteredText];
label.text = [NSString stringWithFormat:@"You typed: %@", entered];
}

}


You can download a sample project that shows how it works right here.

Note: to those of you wondering if this violates the HIG, or will cause problems during review - I don't think it should. Apple does this themselves when they prompt you for a WiFi network password, and I have not used any private APIs or functions whatsoever. I just subclassed an existing public class and extended its functionality in a way that's commonly done.

The code for the class follows:

AlertPrompt.h
//
// AlertPrompt.h
// Prompt
//
// Created by Jeff LaMarche on 2/26/09.

#import <Foundation/Foundation.h>

@interface AlertPrompt : UIAlertView
{
UITextField *textField;
}

@property (nonatomic, retain) UITextField *textField;
@property (readonly) NSString *enteredText;
- (id)initWithTitle:(NSString *)title message:(NSString *)message delegate:(id)delegate cancelButtonTitle:(NSString *)cancelButtonTitle okButtonTitle:(NSString *)okButtonTitle;
@end



AlertPrompt.m
//
// AlertPrompt.m
// Prompt
//
// Created by Jeff LaMarche on 2/26/09.

#import "AlertPrompt.h"

@implementation AlertPrompt
@synthesize textField;
@synthesize enteredText;
- (id)initWithTitle:(NSString *)title message:(NSString *)message delegate:(id)delegate cancelButtonTitle:(NSString *)cancelButtonTitle okButtonTitle:(NSString *)okayButtonTitle
{

if (self = [super initWithTitle:title message:message delegate:delegate cancelButtonTitle:cancelButtonTitle otherButtonTitles:okayButtonTitle, nil])
{
UITextField *theTextField = [[UITextField alloc] initWithFrame:CGRectMake(12.0, 45.0, 260.0, 25.0)];
[theTextField setBackgroundColor:[UIColor whiteColor]];
[self addSubview:theTextField];
self.textField = theTextField;
[theTextField release];
CGAffineTransform translate = CGAffineTransformMakeTranslation(0.0, 130.0);
[self setTransform:translate];
}

return self;
}

- (void)show
{
[textField becomeFirstResponder];
[super show];
}

- (NSString *)enteredText
{
return textField.text;
}

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

@end

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