Apr 29, 2013

iPad Style Action Sheet For iPhone

Overview

Sometimes you might want to have the same style controls for both iPhone and iPad version of your app. Or you might just need iPad style behavior where tap outside of action sheet discards it. That's when creating your own control might be useful.

Update: Another common task is to add icons to buttons, this feature is now implemented as well.

This tutorial is very similar to creating a custom Alert View, but UITableView is used instead of buttons since Action Sheet tends to have more buttons which are easier to style as table view cells.

Creating custom action sheet is not difficult and involves implementing the following features:
  1. Scaling background picture properly
  2. Managing buttons placement, adding scroll in case they don't fit
  3. Hiding action sheet in case user taps outside of it
  4. Passing button id to delegate when button was pressed
 Here's how custom acton sheet will look like:


1. Setting up the project 

Open XCode and create new project using Single View Application template: 


Name the project, select "iPhone" option from "Devices" menu, make sure that "Use Automatic Reference Counting" is checked, press "Next" one more time and save your project.

Now create new file (shortcut CMD+N) which inherits from UIView and name it CustomActionSheet. 

2. Adding graphics

In order to create custom action sheet, you will need to include the following files to your project:

1. Background picture:

action_bg.png

2. Button (table view cell background):

button.png
2. Icons (in separate files):


Add those pictures to your project as well (you can replace them with the ones you need), but please note that @2x version of the files should be added for Retina display support.

3. Adding functionality 

Setting up CustomActionSheet.h

  • CustomActionSheet class is going to be UITableView's delegate and data source
  • CustomActionSheetDelegate should be created so that delegate could be informed about button pressed 
  • Title, array containing button names and array containing icons (in case of no icons Nil should be used) should be passed during initialization along with delegate
CustomActionSheet.h file should have the following structure:

#import <UIKit/UIKit.h>

@interface CustomActionSheet : UIView <UITableViewDelegate, UITableViewDataSource>
{
    NSArray *TableButtons;
    NSArray *TableIcons;
    
    UITableView *ActionSheetTableView;
    
    id delegate;
    
    UIImage *ButtonImg;
    UILabel *TitleLbl;
}
@property id delegate;


- (id)initWithFrame:(CGRect)frame title:(NSString*)title buttons:(NSArray*)buttons icons:(NSArray*)icons delegate:(id)customActionSheetDelegate;

@end

@protocol CustomActionSheetDelegate
- (void)customActionSheet:(CustomActionSheet*)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex;
@end

Setting up CustomActionSheet.m

First of all let's define maximum height allowed for action sheet by adding the following line at the top:

#define MAX_HEIGHT 300.0

Next we should synthesize delegate right before init method:

@synthesize delegate;

Now init method itself should be replaced. New init method will perform the following functions:

  • Creating opaque black background for the whole view
  • Creating background of required height for action sheet
  • Creating table view
Final code looks like this:

- (id)initWithFrame:(CGRect)frame title:(NSString*)title buttons:(NSArray*)buttons icons:(NSArray*)icons delegate:(id)customActionSheetDelegate
{
    self = [super initWithFrame:frame];
    if (self) {
        delegate = customActionSheetDelegate;
        TableButtons = [[NSArray alloc] initWithArray:buttons];
        
        if (icons)
            TableIcons = [[NSArray alloc] initWithArray:icons];
        else
            TableIcons = Nil;
        
        ButtonImg = [UIImage imageNamed:@"button.png"];
        
        self.alpha = 0.0;
        self.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.6];
        self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
        
        UIImage *ActionSheetBg = [UIImage imageNamed:@"action_bg.png"];
        
        if ([ActionSheetBg respondsToSelector:@selector(resizableImageWithCapInsets:)])
            ActionSheetBg = [[UIImage imageNamed: @"action_bg.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(40.0, 25.0, 25.0, 25.0)];
        else
            ActionSheetBg = [[UIImage imageNamed: @"action_bg.png"] stretchableImageWithLeftCapWidth: 25 topCapHeight: 40];
        
        
        UIImageView *BgImgView = [[UIImageView alloc] initWithImage:ActionSheetBg];
        
        TitleLbl = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, BgImgView.frame.size.width-20.0, 0.0)];
        TitleLbl.backgroundColor = [UIColor clearColor];
        TitleLbl.font = [UIFont boldSystemFontOfSize:18.0];
        TitleLbl.textColor = [UIColor whiteColor];
        TitleLbl.numberOfLines = 0;
        TitleLbl.textAlignment = UITextAlignmentCenter;
        TitleLbl.text = title;
        [TitleLbl sizeToFit];
        
        TitleLbl.frame = CGRectMake(0.0, 0.0, BgImgView.frame.size.width-20.0, TitleLbl.frame.size.height+20.0);
        
        BOOL ShouldAllowScroll = NO;
        
        float view_height = (ButtonImg.size.height+10)*[TableButtons count]+TitleLbl.frame.size.height+20.0;
        
        if (view_height>MAX_HEIGHT)
        {
            ShouldAllowScroll = YES;
            view_height = MAX_HEIGHT;
        }
        
        UIView *bgView = [[UIView alloc] initWithFrame:CGRectMake((int)((self.frame.size.width-BgImgView.frame.size.width)/2.0), (int)((self.frame.size.height-view_height)/2.0), BgImgView.frame.size.width, view_height)];
        bgView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
        bgView.backgroundColor = [UIColor clearColor];
        [self addSubview:bgView];
        
        
        BgImgView.frame = bgView.bounds;
        
        [bgView addSubview:BgImgView];
        
        ActionSheetTableView = [[UITableView alloc] initWithFrame:CGRectMake(10.0, 5.0, BgImgView.frame.size.width-20.0, view_height-20.0) style:UITableViewStylePlain];
        ActionSheetTableView.delegate = self;
        ActionSheetTableView.dataSource = self;
        ActionSheetTableView.scrollEnabled = ShouldAllowScroll;
        ActionSheetTableView.backgroundColor = [UIColor clearColor];
        ActionSheetTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
        ActionSheetTableView.rowHeight = ButtonImg.size.height+10;
        [bgView addSubview:ActionSheetTableView];

        [self ShowViewAnimated];

    }
    return self;
}

Next step would be adding table delegate and data source methods. Basically name of our action sheet would be title of the table view, and action sheet's buttons are going to be table view cells. When user selects a cell, delegate receives index of a button pressed:

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    return [TableButtons count];
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *CellIdentifier = [NSString stringWithFormat:@"row%dsection%d", indexPath.row, indexPath.section];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;

        UIImageView *bgImgView = [[UIImageView alloc] initWithImage:ButtonImg];
        bgImgView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
        bgImgView.center = cell.center;
        [cell addSubview:bgImgView];
        
        UILabel *CellTitleLbl = [[UILabel alloc] initWithFrame:bgImgView.bounds];
        CellTitleLbl.backgroundColor = [UIColor clearColor];
        CellTitleLbl.text = [TableButtons objectAtIndex:indexPath.row];
        CellTitleLbl.font = [UIFont boldSystemFontOfSize:18.0];
        CellTitleLbl.textColor = [UIColor blackColor];
        CellTitleLbl.textAlignment = UITextAlignmentCenter;
        CellTitleLbl.adjustsFontSizeToFitWidth = YES;
        [bgImgView addSubview:CellTitleLbl];
        
        if (TableIcons)
        {
            UIImageView *iconImgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:[TableIcons objectAtIndex:indexPath.row]]];
            
            //if icon is too big, we will scale it
            int max_height = bgImgView.frame.size.height-10.0;
            
            if (iconImgView.frame.size.height>max_height)
                iconImgView.frame = CGRectMake(0.0, 0.0, iconImgView.frame.size.width*max_height/iconImgView.frame.size.height, max_height);
            
            iconImgView.frame = CGRectMake(70.0, (int)(bgImgView.frame.size.height-iconImgView.frame.size.height)/2.0, iconImgView.frame.size.width, iconImgView.frame.size.height);
            
            [bgImgView addSubview:iconImgView];
            
            CellTitleLbl.frame = CGRectMake(iconImgView.frame.origin.x+iconImgView.frame.size.width+10.0, 0.0, bgImgView.frame.size.width-iconImgView.frame.origin.x-iconImgView.frame.size.width-90.0, bgImgView.frame.size.height);
            CellTitleLbl.textAlignment = UITextAlignmentLeft;
        }
    }
    
    return cell;
}


#pragma mark - Table view delegate

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
    return TitleLbl.frame.size.height;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    return TitleLbl;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    
    [delegate customActionSheet:self clickedButtonAtIndex:indexPath.row];
    [self removeFromSuperview];
}

In case user taps outside of table view, delegate should be informed that action sheet was dismissed. Let's use index = -1 for this case:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [delegate customActionSheet:self clickedButtonAtIndex:-1];
    [self removeFromSuperview];
}

Since we used table view to implement action sheet, touches even would be sent only if user taps outside of table view.

Now, the last thing would be to add some show animation:

-(void)ShowViewAnimated
{
    self.alpha = 0.0;
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:0.1];
    self.alpha = 0.95;
    [UIView commitAnimations];
}

Setting up ViewController.h

Basically we just need to import CustomActionSheet.h and set view controller to be its delegate. Here's how the code should look like:

#import <UIKit/UIKit.h>
#import "CustomAlert.h"

@interface ViewController : UIViewController <CustomActionSheetDelegate>

@end

Setting up ViewController.m

Let's add button by pasting the following code in viewDidLoad function (you can also use interface builder instead of manual code):


UIButton *ActionSheetBtn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    ActionSheetBtn.frame = CGRectMake((int)((self.view.frame.size.width-200.0)/2.0), 240.0, 200.0, 50.0);
    [ActionSheetBtn setTitle:@"Show Action Sheet" forState:UIControlStateNormal];
    [ActionSheetBtn addTarget:self action:@selector(onActionSheetBtnPressed) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:ActionSheetBtn];
    ActionSheetBtn.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;

Now let's add alert in onActionSheetBtnPressed implementation:


- (void)onActionSheetBtnPressed
{
    CustomActionSheet *actionSheet = [[CustomActionSheet alloc] initWithFrame:self.view.bounds title:@"Set background color:" buttons:[NSArray arrayWithObjects:@"Red", @"Green", @"Blue", @"Orange"/*, @"Purple"*/, nil] delegate:self];
    [self.view addSubview:actionSheet];
}

And the final step would be implementing delegate method:


- (void)customActionSheet:(CustomActionSheet*)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
    switch (buttonIndex) {
        case -1://cancel
            break;
        case 0://Red
            self.view.backgroundColor = [UIColor colorWithRed:0.99 green:0.66 blue:0.65 alpha:1.0];
            break;
        case 1://Green
            self.view.backgroundColor = [UIColor colorWithRed:0.8 green:1.0 blue:0.69 alpha:1.0];
            break;
        case 2://Blue
            self.view.backgroundColor = [UIColor colorWithRed:0.82 green:0.9 blue:1.0 alpha:1.0];
            break;
        case 3://Orange
            self.view.backgroundColor = [UIColor colorWithRed:1.0 green:0.81 blue:0.62 alpha:1.0];
            break;
        default:
            break;
    }
}

4. Conclusions


The main advantages of using UITableView are following:
  • Scrolling is managed automatically
  • Easy to create a few groups of buttons by using table view's sections
  • Easy to modify buttons style in one place since only UITableViewCell has to be changed. For example, it's easy to add picture to each table row along with text
  • Easy to track touches outside of table view

You can download source code here


1 comment: