Starter Step

Archive for March 2009

For UISpec I needed a an easy way to create and cache a method call on an object. I know that NSInvocation is the class to go with, but it’s very cumbersome to create and use. Luckily I found the blog post Callable Objects in Cocoa, which pointed me in the right direction, so I took what they suggested one step further and made it dynamic using message forwarding.

Say you have an address book object that models a list of addresses and you want to “record” a call that does a find by first and last name and “play” the call later.


//Create a Recordable instance with the addressBook as its target
Recordable *dvd = [Recordable withTarget:addressBook];

//Record the call you want to make
[dvd findAddressByFirstName:@"Joe" lastName:@"Smith"];

//Play it to get the results of the call
Address *address = dvd.play;

Pretty nifty huh? Here is the source code. Enjoy!

Recordable.h


@interface Recordable : NSObject {
	id target, play;
	NSInvocation *invocation;
}

@property(nonatomic, retain) id target;
@property(nonatomic, retain) NSInvocation *invocation;
@property(nonatomic, readonly) id play;

+(id)withTarget:(id)target;

@end

Recordable.m

<ol>
  <li>import "Recordable.h"</li>
</ol>

@implementation Recordable

@synthesize target, invocation;

+(id)withTarget:(id)target {
	return [[[self alloc] initWithTarget:target] autorelease];
}

-(id)initWithTarget:(id)_target {
	if (self = [super init]) {
		self.target = _target;
	}
	return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
	return [target methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation setTarget:target];
    [anInvocation retainArguments];
    [anInvocation setReturnValue:&self];
    self.invocation = anInvocation;
}

-(id)play {
	id value;
	[invocation invoke];
	NSString *returnType = [NSString stringWithFormat:@"%s", [[invocation methodSignature] methodReturnType]];
	if (![returnType isEqualToString:@"v"]) {
		id value;
		[invocation getReturnValue:&value];
		return value;
	} else {
		return nil;
	}
}

-(void)dealloc {
	self.target = nil;
	self.invocation = nil;
	[super dealloc];
}

@end

Tags: ,

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

We are happy to announce the availability of a new UISpec demo along with a first pass at documentation, and it’s actually pretty thorough.

  • The documentation can be found here.
  • To run the demo just do an svn checkout or export explained here. Then within the folder you did a checkout/export, go to the directory xcode/UISpecDemo and open the file UISpecDemo.xcodeproj to startup XCode, and then click “Build and Go”. If you have any issues please post them in the UISpec Group.

We are getting close to releasing our new open source behavior driven development framwork for the iPhone called UISpec. It is modeled after RSpec for Ruby, but is optimized for Objective-C programming with UIKit. We have the new google code site up which you can check out here. More to come soon.

If you have ever dealt with using a UINavigationController in your iPhone app, you may have wanted to swap out the root view controller with a new one. Unfortunately the iPhone SDK doesn’t allow you to do this. To do so you actually have to create a brand new UINavigationController, which for us was not acceptable.

To support swapping out the root view controller, we created a custom class that extends UINavigationController and adds a method “setRootViewController”. We do a little hack by creating a generic UIViewController and setting it as a fake root view controller at index 0. This allows you to push and pop UIViewControllers above it on the stack. So now the “perceived” root view controller is always at index 1 behind the scenes, but actually appears to be at index 0. It behaves exactly as the original UINavigationController except that now you can change the “perceived” root view controller. Here is an example of using it:

//Say you have a UIViewController that 
//handles logging in to your app
self.loginViewController = [LoginViewController loginViewController];

//Here we initialize it as the root view controller
//of your main navigation
self.mainNav = [[[TVNavigationController alloc] initWithRootViewController:loginViewController] autorelease];

//Let's say the user interacts with the screen and logs 
//in and now you want to replace the login view controller 
//with the UIViewController that is your application

self.appViewController = [AppViewController appViewController];
//Now we call the new method that let's you set 
//the root view controller 
[mainNav setRootViewController:appViewController];

Below is the code to make all this work:

//// Header file

@interface TVNavigationController : UINavigationController {
  UIViewController *fakeRootViewController;
}

@property(nonatomic, retain) UIViewController *fakeRootViewController;

-(void)setRootViewController:(UIViewController *)rootViewController;

@end
//// Implementation file

#import “TVNavigationController.h”

@implementation TVNavigationController

@synthesize fakeRootViewController;

//override the standard init
-(id)initWithRootViewController:(UIViewController *)rootViewController {
  //create the fake controller and set it as the root
  UIViewController *fakeController = [[[UIViewController alloc] init] autorelease];
  if (self = [super initWithRootViewController:fakeController]) {
    self.fakeRootViewController = fakeController;
    //hide the back button on the perceived root
    rootViewController.navigationItem.hidesBackButton = YES;
    //push the perceived root (at index 1)
    [self pushViewController:rootViewController animated:NO];
  }
  return self;
}

//override to remove fake root controller
-(NSArray *)viewControllers {
    NSArray *viewControllers = [super viewControllers];
	if (viewControllers != nil && viewControllers.count > 0) {
		NSMutableArray *array = [NSMutableArray arrayWithArray:viewControllers];
		[array removeObjectAtIndex:0];
		return array;
	}
	return viewControllers;
}

//override so it pops to the perceived root
- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated {
    //we use index 0 because we overrided “viewControllers”
    return [self popToViewController:[self.viewControllers objectAtIndex:0] animated:animated];
}

//this is the new method that lets you set the perceived root, the previous one will be popped (released)
-(void)setRootViewController:(UIViewController *)rootViewController {
    rootViewController.navigationItem.hidesBackButton = YES;
    [self popToViewController:fakeRootViewController animated:NO];
    [self pushViewController:rootViewController animated:NO];
}

- (void)dealloc {
    self.fakeRootViewController = nil;
    [super dealloc];
}

@end
Tags:


  • 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