Look! UIAlertView is dating UITableView!

As you may have guessed, this is about UIAlertView containing a UITableView. Since I started playing around with GameKit, I had the issue that I couldn’t use the PeerPicker with Client/Server stuff..
I can’t really get into the stuff we are doing and where we are using it – but I can offer you some trick and code to have a UIAlertView displayed with a fully controllable UITableView.

I started off with making it a decorator.. After 20′ I had to give up, because it was just too complicated to decorate objects where you don’t really know what’s going on. So I had to subclass it – unfortunately, but anyway..

Let’s take a look at what you would probably like to have..

Client

UIAlertTableView *alert = [[UIAlertTableView alloc] initWithTitle:@"Select Option"
    message:@"select option or create one"
    delegate:self
    cancelButtonTitle:@"Cancel"
    otherButtonTitles:@"Create", nil];
alert.tableDelegate = self;
alert.dataSource = self;
alert.tableHeight = 120;

And this should be the result:

Alright, so, we just add a UITableView to the UIAlertView as a Subview, right? Hold on, Tiger :)
First of all, if we set the message to nil, we want to have this:

And if we rotate, we want to have the nice effects! Like this:

Rotate Animation

If you only want to grab the code without BlahBlah: UIAlertTableView on Bitbucket.org (btw. I switched to bitbucket.org – but that’s another story)

So, let’s look at the code a bit:

UIAlertTableView.h


#import <UIKit/UIKit.h>

@class UIAlertView;

@interface UIAlertTableView : UIAlertView {
	// The Alert View to decorate
	UIAlertView *alertView;

	// The Table View to display
	UITableView *tableView;

	// Height of the table
	int tableHeight;

	// Space the Table requires (incl. padding)
	int tableExtHeight;

	id dataSource;
	id tableDelegate;
}

@property (nonatomic, assign) id dataSource;
@property (nonatomic, assign) id tableDelegate;

@property (nonatomic, readonly) UITableView *tableView;
@property (nonatomic, assign) int tableHeight;

- (void)prepare;

@end

You see: we subclass UIAlertView, we have a UITableView, we have a delegate which needs to implement the protocol as well as a dataSource. Straight forward, I’d say…

Now, as for the implementation, one would think: just overload the “show” method and insert the TableView as a subview etc. Well, that works – NOT. First of all: you have to resize your AlertView, then you have to move the buttons down and then you have to place the tableView somewhere (esoteric?).
OkOk, just overwrite the drawRect then? You are getting closer!
But, first, let’s have a look at the prepare method.

UIAlertTableView.m:prepare


- (void)prepare {
	if (tableHeight == 0) {
		tableHeight = 150.0f;
	}

	// Calculate the TableViewHeight with padding
	tableExtHeight = tableHeight + 2 * kTablePadding;

	tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
	tableView.delegate = tableDelegate;
	tableView.dataSource = dataSource;	

	// Insert it as the first subview
	[self insertSubview:tableView atIndex:0];
}

This code creates the TableView but does not set a real frame yet. It sets the given DataSource and Delegate. To be totally correct, there should be a custom setTableDelegate / setDataSource method which changes them in the tableView – but this is left as an exercise to the reader :)
After the creation, we insert the tableView as the very first subview of the alertView – so we know where to find it again and that nothing is hidden because of the tableView.

Now comes the tricky part: drawing.
For that, we use a private API call to the AlertView, called layoutAnimated:(BOOL)animated.
We overload it in our custom subclass because the initial drawing and the drawing on setNeedsLayout goes through that method.
After that method is called, all the elements that belong to the AlertView (title, message, buttons …) are arranged, so we can use those values for our next computations.

So, this is how it goes:

UIAlertTableView.m:layoutAnimated


- (void)layoutAnimated:(BOOL)fp8 {
	[super layoutAnimated:fp8];
	[self setFrame:CGRectMake(self.frame.origin.x, self.frame.origin.y - tableExtHeight/2, self.frame.size.width, self.frame.size.height + tableExtHeight)];

	// We get the lowest non-control view (i.e. Labels) so we can place the table view just below
	UIView *lowestView = [self.subviews objectAtIndex:0];
	int i = 0;
	while (![[self.subviews objectAtIndex:i] isKindOfClass:[UIControl class]]) {
		UIView *v = [self.subviews objectAtIndex:i];
		if (lowestView.frame.origin.y + lowestView.frame.size.height < v.frame.origin.y + v.frame.size.height) {
			lowestView = v;
		}

		i++;
	}

	// TODO: calculate this value
	CGFloat tableWidth = 262.0f;

	tableView.frame = CGRectMake(11.0f, lowestView.frame.origin.y + lowestView.frame.size.height + 2 * kTablePadding, tableWidth, tableHeight);

	for (UIView *sv in self.subviews) {
		// Move all Controls down
		if ([sv isKindOfClass:[UIControl class]]) {
			sv.frame = CGRectMake(sv.frame.origin.x, sv.frame.origin.y + tableExtHeight, sv.frame.size.width, sv.frame.size.height);
		}
	}
}

Step by step: first, we call the superclass - so everything gets arranged.
Then, we set a new frame for the AlertView, which is the same frame + tableHeight + padding. But we also need to rearrange the frame - which is half of that additional height.
After that, we compute the lowest "sitting" normal view - no control object - in the alert view. We loop through it, until we find the first control element - which are usually the buttons, because they are added at the end. You could loop through all views and get the lowest from any non-UIControl objects. But this works :)
This position is used to place the TableView at the correct position - whether you have a message, a title or whatever there.. but it needs to be above the buttons - UI standards.
To be above the buttons, we need to rearrange the buttons, and this is what happens: all UIControl object in the AlertView are moved down by the size we added to the AlertView frame - the complete TableHeight. And this is it.
We resize the AlertView, calculate the position where the TableView is supposed to be inserted at and then move the buttons. If we rotate, this gets calculated again and nicely rearranged.

Note: you can apply the very same method for any other UIView object you want to insert into an UIAlertView.. Be it TextField or ImageView or whatever. (For textField, you should use their private APIs though)

Again, code is here: Code on Bitbucket.org

Dare to comment?