[Tutorial] Embedding Tables Into UIAlertViews

Discussion in 'iOS Development' started by SkylarEC, Oct 10, 2009.

  1. SkylarEC

    SkylarEC Super Moderator Emeritus Staff Member

    Joined:
    Sep 19, 2007
    Messages:
    6,657
    Likes Received:
    125
    Original Post: http://www.skylarcantu.com/blog/2009/10/10/embedding-uitables-into-uialertviews/

    The newer Apple firmware will create a table in your UIAlertView automatically if you create a more than the number of buttons that fit onto the alert. Note that this number is different in landscape and portrait modes. I found the implementation to be very unappealing for a few reasons. First of all, you won't really know if you will have buttons or a table until after you compile and test the application. Plus, if you want to display a table, but you only have a few options, then there's no real way to get that with the standard UIAlertView. And finally, what if you had wanted to customize the table with colored cells, custom cells with images on them, or take advantage of the editing properties of the table and its "Swipe to delete" feature? What if you want a table with multiple sections? You would be able to do none of the above with the standard UIAlertView.

    So instead, I decided the best option would be to create another custom UIAlertView whose sole purpose is to display a table.
    [​IMG]

    If you're reading this, you may notice that I am borrowing a couple of methods from my previous custom UIAlertView post, Custom UIAlertView (Color chooser), which has become one of the more popular posts on my site. The great thing about decent code is its reusability. But, we'll get to the alert in a little bit.

    Before we start with the alert, we need to get a quick application built that will utilize the embedded table alert. I will guide you through the steps for a simple application that builds an array of strings which will be used to feed the table's data source.

    Start off by creating a simple view based project in Xcode. Name this UIEmbeddedTableAlert, or whatever. Go ahead and add three buttons and a text label wherever you feel they should be on the screen. I did this in the -loadView method so that all the stuffs get loaded when the view gets loaded. You can do this wherever. I also take this opportunity to create a initialize the array that will hold out strings.
    [OBJC]- (void)loadView {
    [super loadView];
    self.view.backgroundColor = [UIColor underPageBackgroundColor];

    stringsArray = [NSMutableArray new];
    Class $UIGlassButton = objc_getClass("UIGlassButton");
    //Alert button
    UIGlassButton *alertButton = [[$UIGlassButton alloc] initWithFrame:CGRectMake(20, 403, 280, 47)];
    alertButton.tintColor = [UIColor colorWithRed:0.00 green:0.00 blue:0.27 alpha:0.85];
    [alertButton setTitle:mad:"Show Alert" forState:UIControlStateNormal];
    [alertButton addTarget:self action:mad:selector(alertButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:alertButton];
    [alertButton release];

    //Add string button
    UIGlassButton *addStringButton = [[$UIGlassButton alloc] initWithFrame:CGRectMake(20, 87, 280, 47)];
    addStringButton.tintColor = [UIColor colorWithRed:0.0 green:0.75 blue:0.0 alpha:0.85];
    [addStringButton setTitle:mad:"Add String to Array" forState:UIControlStateNormal];
    [addStringButton addTarget:self action:mad:selector(addStringButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:addStringButton];
    [addStringButton release];

    //Clear button
    UIGlassButton *clearStringsButton = [[$UIGlassButton alloc] initWithFrame:CGRectMake(20, 346, 280, 47)];
    clearStringsButton.tintColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.85];
    [clearStringsButton setTitle:mad:"Clear Strings" forState:UIControlStateNormal];
    [clearStringsButton addTarget:self action:mad:selector(clearStringsButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:clearStringsButton];
    [clearStringsButton release];

    UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(20, 20, 280, 47)];
    textField.adjustsFontSizeToFitWidth = YES;
    textField.borderStyle = UITextBorderStyleRoundedRect;
    textField.clearButtonMode = UITextFieldViewModeWhileEditing;
    textField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
    textField.delegate = self;
    textField.font = [UIFont systemFontOfSize:20];
    textField.keyboardAppearance = UIKeyboardAppearanceDefault;
    textField.placeholder = @"Enter string";
    textField.returnKeyType = UIReturnKeyDone;
    textField.tag = TEXTFIELD_TAG;

    [self.view addSubview:textField];
    [textField release];
    }
    [/OBJC]

    "Uh, oh! WTF is a UIGlassButton?" you're probably asking. Well, it's this brilliant UIButton subclass that Apple includes in UIKit, but doesn't want the regular developer to have access to. They are the buttons that you see in Maps.app. You can use regular buttons, if you want, it makes no difference.

    If you have entered the above code correctly, you will have something that looks like this:
    [​IMG]

    Now that we have our buttons and textfield in place, let's add in their appropriate methods. We'll start with the textfield. Make sure to have your view conform to the UITextFieldDelegate protocol and enter the following code:

    [OBJC]#pragma mark
    #pragma mark UITextFieldDelegate Methods

    - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
    textFieldIsEditing = YES;
    return YES;
    }


    - (void)textFieldDidEndEditing:(UITextField *)textField {
    textFieldIsEditing = NO;
    if ([stringsArray count] > 0) {
    textField.placeholder = @"Enter another string";
    }
    textField.text = nil;
    }


    - (BOOL)textFieldShouldReturn:(UITextField *)textField {
    if (![textField.text isEqualToString:mad:""]) {
    [stringsArray addObject:textField.text];
    }
    [textField resignFirstResponder];

    return NO;
    }[/OBJC]

    The above code is real simple. All we are doing is raising a flag when we begin editing the textfield, and lowering the flag when we are finished. When we have finished, if we actually entered a string, we are adding the string to our stringsArray. Very easy. Now, let's go in and let our app know that if the user touches outside of the box, but not on a button, that we want to stop editing, and to hide the keyboard.

    [OBJC]#pragma mark
    #pragma mark UIResponder Methods

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UIView *view in self.view.subviews) {
    if (view.tag == TEXTFIELD_TAG) {
    [view resignFirstResponder];
    }
    }
    }[/OBJC]

    The only thing we have to worry about here is touches down. When we receive that, we will iterate through the subviews until we find the UITextField (reference its tag) and tell it to resign being the first responder. Resigning being the first responder will make the keyboard hide.

    Next, let us add in the methods that will fire when our buttons are tapped.
    [OBJC]#pragma mark
    #pragma mark UIButton Methods

    - (void)addStringButtonPressed:(UIButton *)button {
    if (textFieldIsEditing) {
    for (UIView *view in self.view.subviews) {
    if (view.tag == TEXTFIELD_TAG) {
    [self textFieldShouldReturn:(UITextField *)view];
    }
    }
    }
    }


    - (void)alertButtonPressed:(UIButton *)button {
    UITableAlert *alert = [[UITableAlert alloc] initWithTitle:mad:"Table Alert" message:nil delegate:self cancelButtonTitle:mad:"Cancel" otherButtonTitles:nil];
    alert.delegate = self;
    [alert show];
    [alert release];
    }


    - (void)clearStringsButtonPressed:(UIButton *)button {
    [stringsArray removeAllObjects];

    for (UIView *view in self.view.subviews) {
    if (view.tag == TEXTFIELD_TAG) {
    ((UITextField *)view).text = nil;
    ((UITextField *)view).placeholder = @"Enter String";
    }
    }

    }[/OBJC]

    As you can see, the button methods are simple. -addStringButtonPressed: takes the textfield's string and adds it to our array. -clearStringsButtonPressed: clears our array of strings and resets the UITextField. -alertButtonPressed: has us allocating and showing our new custom UIAlertView. As you can see, we are going to set it up and use it how we would any other UIAlertView.

    So, while we are here, let's create our custom alert.
    UITableAlert.h
    [OBJC]#import <UIKit/UIKit.h>


    @interface UITableAlert : UIAlertView <UITableViewDelegate> {
    id _tableData;
    @private
    UITableView *table;
    }

    @property (nonatomic, assign, setter=setTableData:) id tableData;
    @end[/OBJC]

    A couple of things to note here. We want our UITableView property to be property because we do not want other objects messing with it. If you were to subclass this, you may want to change the table to @protected. Also, if you notice the property declaration for tableData; we will be overriding the setter for that variable. This is so that we can also reload the table when new data is set.

    But wait, we're feeding the table data from outside the table or table controller? Of course! By taking advantage of the table's .dataSource property, we can keep reusing this alert over and over again in several projects without changing a single line of code. We will create the data source for our table a little later on.

    Right now, let's focus on the implementation for our alert view.
    [OBJC]#import "UITableAlert.h"


    @implementation UITableAlert

    @synthesize tableData = _tableData;


    - (id)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
    table = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
    table.backgroundColor = [UIColor colorWithWhite:0.90 alpha:1.0];
    table.delegate = self;
    }
    return self;
    }

    - (void)setFrame:(CGRect)rect {
    [super setFrame:CGRectMake(0, 0, rect.size.width, 310)];
    self.center = CGPointMake(320/2, 480/2);
    }


    - (void)layoutSubviews {
    CGFloat buttonTop;
    for (UIView *view in self.subviews) {
    if ([[[view class] description] isEqualToString:mad:"UIThreePartButton"]) {
    view.frame = CGRectMake(view.frame.origin.x, self.bounds.size.height - view.frame.size.height - 15, view.frame.size.width, view.frame.size.height);
    buttonTop = view.frame.origin.y;
    }
    }

    buttonTop -= 7; buttonTop -= 200;

    UIView* container = [[UIView alloc] initWithFrame:CGRectMake(12, buttonTop, self.frame.size.width - 53, 200)];
    container.backgroundColor = [UIColor whiteColor];
    container.clipsToBounds = YES;
    [self addSubview:container];

    table.frame = container.bounds;
    [container addSubview:table];

    UIGraphicsBeginImageContext(container.frame.size);
    CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 2.0);
    CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0.9, 0.9, 0.9, 1.0);
    CGContextSetShadow(UIGraphicsGetCurrentContext(), CGSizeMake(0, -3), 3.0);
    CGContextStrokeRect(UIGraphicsGetCurrentContext(), CGContextGetClipBoundingBox(UIGraphicsGetCurrentContext()));
    UIImageView *imageView = [[UIImageView alloc] initWithImage:UIGraphicsGetImageFromCurrentImageContext()];
    [container addSubview:imageView];
    [imageView release];
    UIGraphicsEndImageContext();
    }


    - (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    }


    - (void)dealloc {
    [table release];
    [super dealloc];
    }[/OBJC]

    I go into much more detail on these methods in my previous custom alertview post. So to skim, we override the initialization method and set up the basics of the alert. In this case, we will need to create and set up a UITableView. -setFrame: is our override that we use to set the size of the UIAlertView. Attempting to set any other way will result in failures. And, we use -layoutSubviews to position and size all our subviews.

    We are nearly done with the actual alert itself. The next thing we need to do is create a setter for the .tableData property. While we are thinking of properties, let's go ahead and override the setter and getter for the .message property. This way we don't accidentally set any messages to the body of our alert.

    [OBJC]#pragma mark
    #pragma mark Property Methods

    - (void)setMessage:(NSString *)message {
    return;
    }

    - (NSString *)message {
    return nil;
    }

    - (void)setTableData:(id)tableData {
    _tableData = tableData;
    table.dataSource = _tableData;
    [table reloadData];
    }[/OBJC]

    If you notice, I don't do much with the delegation for the UITableView. That's because you have a couple of options. You can add another property to your alert view called tableDelegate, which will function exactly the same as your tableData property. Doing so will allow you to set custom delegates to your tables. If you do this, your work with the custom alert is done.

    If you opt to go another route with the table delegation, you still have a little work left to do. The other option is to set the alertView itself as the table's delegate object. You can then intercept the table's -didSelectRowAtIndexPath: method and pass that method onto the alertview's delegate. You will, of course, have to create another protocol for your alertview, maybe <UITableAlertTableDelegate>, or something similar.

    I am leaving that up to you, as one choice is not better than the other.

    Now, we are nearly done. We only have to create one more object: the table's data source. This is actually going to be the simplest object we've had to make so far.

    UITableAlertDataSource.h
    [OBJC]#import <Foundation/Foundation.h>
    #import <UIKit/UIKit.h>


    @interface UITableAlertDataSource : NSObject <UITableViewDataSource> {
    NSMutableArray *_data;
    }

    @property (nonatomic, retain) NSMutableArray *data;

    @end[/OBJC]

    As you can see, the data source is just a simple subclass of NSObject that contains some data and conforms to the UITableViewDataSource protocol. We will implement this object like so:
    [OBJC]#import "UITableAlertDataSource.h"


    @implementation UITableAlertDataSource
    @synthesize data = _data;

    - (id)initWithArray:(NSMutableArray *)array {
    if (self = [super init]) {
    self.data = [array mutableCopy];
    }

    return self;
    }

    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
    }


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


    - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    return nil;
    }


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

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
    cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

    switch (indexPath.section) {
    case 0:
    cell.textLabel.text = [_data objectAtIndex:indexPath.row];
    break;
    default:
    break;
    }

    return cell;
    }
    [/OBJC]

    Since the table we are working with is a simple list of strings, we don't need to do much at all; just set the appropriate string for the table cell's textLabel when the table requests. If you want to get fancy with your table and use custom table cells, fancy table headers, or <em>anything</em> that you can do on or in a standard table, this is where you'd do it.

    Now, let's put this datasource object to use. Go back to your view controller and conform to <UIAlertViewDelegate> in the header. Go into its implementation and add the following method:
    [OBJC]#pragma mark
    #pragma mark UITableAlert Methods

    - (void)didPresentAlertView:(UIAlertView *)alertView {
    id data = [[UITableAlertDataSource alloc] initWithArray:stringsArray];
    ((UITableAlert *)alertView).tableData = data;
    }[/OBJC]

    What we are doing is waiting until after the alert is presented to feed it the data source. The reason being that we want to pace ourselves. If we have the alert load the data into its table while it is trying to animate out onto the screen, the application may get skippy or bogged down.

    Essentially, if you have followed all these steps, you are done. Congratulations!



    This project can be downloaded by visiting: skylarcantu.com
  2. NAGARAJU

    NAGARAJU New Member

    Joined:
    Aug 9, 2012
    Messages:
    1
    Likes Received:
    0
    Device:
    iPhone 4S (Black)
    PLEASE SEND THIS CODE TO MY MAIL …...
  3. Cameron E

    Cameron E New Member

    Joined:
    Aug 12, 2012
    Messages:
    4
    Likes Received:
    0
    Device:
    iPhone 4 (Black)
    Skylar....whoever you are. You are awesome.
    I hope you know that.

Share This Page