Starter Step

Archive for the ‘TripCase’ Category

Brian and I have been working on improving the startup of TripCase and have found some interesting items. Our startup on TripCase slow…eventually we think we will move to a more facebook style model where much of your data is stored locally on the device. This will allow us to show the main view of TripCase without the 6-N seconds of time to talk to the server first. While the page loads with the cached data we can asyncronously retrieve the live data and update the view….but I digress.

We were debugging our message stream on the main view and noticed that the first cell was taking almost a full second to create. All subsequent cells were taking on the order of .15 – .20 seconds. After some NSLogs here and there we finally tracked down the culprit! In our app we implemented a “time ago” feature that tells you relatively how long ago something occurred. It is a nice feature and lets users see that something was created a few minutes ago, a few hours ago, etc. Its pretty granular and makes for a good experience. The times we convert to timeago are given to the client through our server and are all in GMT. This means that in timeago we need to create a NSDateFormatter with a mask that includes timezone. Our poor performing code looked something like the following:

+(NSString *)timeAgo:(NSString *)_date {
	static NSDateFormatter *parser = nil;
	if (parser == nil) {
		parser = [[NSDateFormatter alloc] init];
                [parser setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"] autorelease]];      
		[parser setFormatterBehavior:NSDateFormatterBehavior10_4];
		[parser setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss z"];
	}
	NSDate *date = [parser dateFromString:[_date stringByAppendingString:@" GMT"]];
	double littleOverAWeek = 11520.0f;
	NSDate *now = [NSDate date];
	double deltaMinutes = fabs([date timeIntervalSinceDate:now]) / 60.0f;
	if(deltaMinutes < littleOverAWeek){
		return [NSString stringWithFormat:@"%@ ago", [self distanceOfTimeInWords:deltaMinutes]];
	} else {
		return [NSString stringWithFormat:@"on %@", [DateFormatter shortDateFromDate:date]];
	}
}

In the above method, we create a NSDateFormatter with a mask of “yyyy-MM-dd’T’HH:mm:ss z”. Seems pretty harmless right? Wrong! Brian and I removed the “z” and timed the creation of the NSDateFormatter. We saw that with the “z” in the mask, the creation of the NSDateFormatter was .7 seconds slower!! 700 milleseconds is a lifetime. We saw that during startup we create two NSDateFormatters that were doing this…that is 1.5 seconds of wasted time that we wanted to remove.

In the docs we noticed that you can set the timezone for the NSDateFormatter explicitly. We did that and the .7 seconds of invocation time went away…BAM! We just shaved 1.5 seconds from our startup time. Our new code looks something like this:

+(NSString *)timeAgo:(NSString *)_date {
	static NSDateFormatter *parser = nil;
	if (parser == nil) {
		parser = [[NSDateFormatter alloc] init];
                [parser setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"] autorelease]];       
		[parser setFormatterBehavior:NSDateFormatterBehavior10_4];
		[parser setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss"];
		[parser setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];
	}
	NSDate *date = [parser dateFromString:_date];
	double littleOverAWeek = 11520.0f;
	NSDate *now = [NSDate date];
	double deltaMinutes = fabs([date timeIntervalSinceDate:now]) / 60.0f;
	if(deltaMinutes < littleOverAWeek){
		return [NSString stringWithFormat:@"%@ ago", [self distanceOfTimeInWords:deltaMinutes]];
	} else {
		return [NSString stringWithFormat:@"on %@", [DateFormatter shortDateFromDate:date]];
	}
}

Two things to notice in the above code:
1. We set the timezone context directly on the NSDateFormatter which allowed us to remove the “z” from the mask.
2. Doing this also allowed us to remove the ugly code that was appending GMT to the time returned from the server.

All of our timings were done on a 3G device. In all we shaved roughly 1.5 seconds because this pattern was used in two places during startup. Score 1 for super sleuthing!

Last night Apple’s latest commercial premiered with our mobile travel app, TripCase, as the first application demoed in the advertisement.

It ran during Heroes, House, and Gossip Girl, and Apple plans on running it for the next several months in various time slots.

If you haven’t seen it yet you can view it on Apple’s website here:

http://www.apple.com/iphone/gallery/ads/#dine-large

I did a write up a while back on a custom UIAlertView that displays a list of options to the user. I’ve gotten some questions on how to use the component so this is a quick write up on just how to do that. Let me first say that I have not done iPhone development for a few months….I have been hard at work getting TripCase (http://tripcase.com) working on Windows Mobile. I hopped back into XCode this evening and felt lost!

So to recap the component, the intent is that you have a list of options you want to show your user. In our case we are geocoding a location that can be resolved to multiple places. We want our user to choose from a list of places returned from our geocoding service. We could have taken the user to another page and had them make a selection and then taken them back to the original page. We weren’t crazy about that…so we rolled this little gem.

Also, just as an fyi, this may not even be needed anymore. I have not looked at how the 3.x sdk would handle this. There might be a nicer approach to this all together. But for those who have asked, here is a sample ApplicationDelegate class body that should get you going. I’ll also attach a zipped up version of the sample app for you to download and use.

- (void)applicationDidFinishLaunching:(UIApplication *)application {    

    // here we just create a button to invoke our alert view.  this of course would be done by some event in your code somewhere.
	UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
	button.frame = CGRectMake(60, 100, 200, 30);
	[button setTitle:@"Invoke the Alert View" forState:UIControlStateNormal];
	[button addTarget:self action:@selector(createAlertView) forControlEvents:UIControlEventTouchUpInside];
	[window addSubview:button];
    [window makeKeyAndVisible];
}

-(void)createAlertView {
	[[[[AlertTableView alloc] initWithCaller:self data:[NSArray arrayWithObjects:@"one", @"two", @"three", @"four", @"five", @"six", nil] title:@"Did you mean..." andContext:nil] autorelease] show];
}


-(void)didSelectRowAtIndex:(NSInteger)row withContext:(id)context {
	[[[UIAlertView alloc] initWithTitle:@"Selection Made" message:[NSString stringWithFormat:@"Index %d clicked", row] delegate:self cancelButtonTitle:@"Ok" otherButtonTitles:nil] show];
}

//-(UIView*) viewForOptionAtRow:(NSUInteger)row {
//}

This sample app just creates a button and throws it on the screen. The click handler for the button is where the use of the new AlertTableView comes into play. The AlertTableView is created with an array of option strings. The caller is “self” because I want this same class to handle the callbacks for the component. I am passing in a nil context because I dont need a context for this small example. I then show the AlertTableView as you would a regular UIAlertView.

Once we have created our new alert, there are two important delegate methods we have at our disposal. The first is didSelectRowAtIndex which will be called when the user clicks either an option in our list, or the cancel button. This method will receive the index of the selected row or -1 if cancel is clicked. It will also get the context that was passed into the creation of the alert. This is where you actually “do” something based on your users selection. The other method you optionally implement is viewForOptionAtRow. This method gives you the ability to customize the view that is displayed in the option list.

That’s pretty much it…. Of course the component needs work..it uses absolute postioning…and makes some bad assumptions…but it gets the job done.

Link to project
Ok so wordpress wont let me upload a zip file…? So I just googled for a free file hosting service and dumped it here Example App.

So yeah…its known that I’m a Mac guy and have sworn off Windows. However, my recent project has me building TripCase on the Windows Mobile platform. We have already built for the iPhone. Brian is hard at work on the BlackBerry version. Needless to say…we are swamped. Anyhow, I digress.

So working with C# has been actually interesting. I am finding that I “like” the language. It reminds me of Java, done better. It has features that are kinda ruby like. I have also been using Linq. Its a query language for parsing all types of enumerables. For TripCase we built RESTful webservices that all of our clients interact with. With that said, we deal alot with XML on this project. Linq has actually been a big help. I’ll try to post some examples of its use later. I’m sure you uber Linq users will laugh at my elementary understanding…but hey…for us guys getting started…examples rock!!

Our awesome travel app TripCase has now launched. Brian and I both work in the Travel Studios team at Sabre and we have been working on our killer travel app for the iPhone since December.  We submitted it to Apple about 4 weeks ago and it finally got approved yesterday and showed up in the AppStore early evening.  Learn more about how TripCase is the Ultimate toolkit to help you stay organized, informed, and connected when you travel by visiting http://www.tripcase.com/.

I have been working on a story: “As a user, I should be able to search for hotels around a given location so that I can add the hotel to my trip.”.

I built my basic form for the user to fill out the location information and provided my awesome continue button to get hotels:

locationform

I quickly realized that the user’s location could be resolved to more than a single place.  If the user entered in county: US and then city: Greenville, there would be 10 or so matches.  I would need the user to choose which was the correct city.  One way to do this would have been to take the user to a new UITableViewController with all the matches and ask the user to choose a match, then pop then continue on to find the hotels around the selected location.  While this would work, I dont like it.  What if it worked like Google Maps?  The Google Maps application shows the user a UIAlertView with an embedded TableView. It’s pretty clean.  It works well because the user does not need to go to a separate page to choose a location match.  I want my user to have this same experience.

If you have used the UIAlertView much, you know it is very limited on custimization opportunity.  I decided to subclass UIAlertView to build my new location selector.  I ran into a few “a ha” moments when trying to mimic the Google Apps look and feel.  First, their UIAlertView has rouned top and bottom corners that are independent of the TableView.  They are actually using a PlainStyle TableView and when you scroll the table in the alert, the table looks like it is sitting inside of a rounded rect UIScrollView.  Tricky Indeed!

Our solution was to use some transparent rounded corners with a dropshadow on the top image.  We also wanted the height of the custom alert to be dynamic based on the size of the embedded UITableView.  However, UIAlertViews do not have a frame or height property. The height of the UIAlertView is based on the length of its message property.  Our solution to this problem was to identify the number of rows in the table and add return characters to the message property of the UIAlertView.

Here is what it ended up looking like:

locationresults

In case you wanna see some code, Here ya go:

@protocol AlertTableViewDelegate

-(void)didSelectRowAtIndex:(NSInteger)row withContext:(id)context;

@end

@interface AlertTableView : UIAlertView <UITableViewDelegate, UITableViewDataSource>{
    UITableView *myTableView;
    id<AlertTableViewDelegate> caller;
    id context;
    NSArray *data;
	int tableHeight;
}
-(id)initWithCaller:(id<AlertTableViewDelegate>)_caller data:(NSArray*)_data title:(NSString*)_title andContext:(id)_context;
@property(nonatomic, retain) id<AlertTableViewDelegate> caller;
@property(nonatomic, retain) id context;
@property(nonatomic, retain) NSArray *data;
@end

@interface AlertTableView(HIDDEN)
-(void)prepare;
@end
#import "AlertTableView.h"



@implementation AlertTableView

@synthesize  caller, context, data;

-(id)initWithCaller:(id)_caller data:(NSArray*)_data title:(NSString*)_title andContext:(id)_context{
    NSMutableString *messageString = [NSMutableString stringWithString:@"\n"];
    tableHeight = 0;
    if([_data count] &lt; 6){
        for(int i = 0; i &lt; [_data count]; i++){
            [messageString appendString:@"\n\n"];
            tableHeight += 53;
        }
    }else{
        messageString = @"\n\n\n\n\n\n\n\n\n\n";
        tableHeight = 207;
    }
    
    if(self = [super initWithTitle:_title message:messageString delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:nil]){
        self.caller = _caller;
        self.context = _context;
        self.data = _data;
        [self prepare];
    }
    return self;
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    [self.caller didSelectRowAtIndex:-1 withContext:self.context];
}

-(void)show{
    self.hidden = YES;
    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(myTimer:) userInfo:nil repeats:NO];
    [super show];
}

-(void)myTimer:(NSTimer*)_timer{
    self.hidden = NO;
    [myTableView flashScrollIndicators];
}

-(void)prepare{
    myTableView = [[UITableView alloc] initWithFrame:CGRectMake(11, 50, 261, tableHeight) style:UITableViewStylePlain];
    if([data count] &lt; 5){
        myTableView.scrollEnabled = NO;
    }
    myTableView.delegate = self;
    myTableView.dataSource = self;
    [self addSubview:myTableView];
    
    UIImageView *imgView = [[[UIImageView alloc] initWithFrame:CGRectMake(11, 50, 261, 4)] autorelease];
    imgView.image = [UIImage imageNamed:@"top.png"];
    [self addSubview:imgView];
    
    imgView = [[[UIImageView alloc] initWithFrame:CGRectMake(11, tableHeight+46, 261, 4)] autorelease];
    imgView.image = [UIImage imageNamed:@"bottom.png"];
    [self addSubview:imgView];

    
    CGAffineTransform myTransform = CGAffineTransformMakeTranslation(0.0, 10);
    [self setTransform:myTransform];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = (UITableViewCell*) [tableView dequeueReusableCellWithIdentifier:@"ABC"];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"ABC"] autorelease];
        cell.selectionStyle = UITableViewCellSelectionStyleBlue;
        cell.font = [UIFont boldSystemFontOfSize:14];
    }
    cell.text = [[data objectAtIndex:indexPath.row] description];
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    [self dismissWithClickedButtonIndex:0 animated:YES];
    [self.caller didSelectRowAtIndex:indexPath.row withContext:self.context];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [data count];
}

-(void)dealloc{
    self.data = nil;
    self.caller = nil;
    self.context = nil;
    [myTableView release];
    [super dealloc];
}

@end


  • Dave: I can tell you're a ruby guy because you forgot the 'return' keyword. Thanks for the tip though!
  • Chandrashekhar H M: Hi, Thanks its working fine in iOS 6 but not in iOS 7.0. Any Suggestion on this.
  • Coeur: To change a rootViewController, without all this TVNavigationController : myNewRoot = [[UIViewController alloc] init]; myNavigationController.view