2. Modal Buttons
Modal controls are UI controls that change state across two or
more steps as part of a single expression of user intent. A good example of a modal control is the “Buy
Now/Install/Installed” button in the mobile App Store application. You
install applications from the App Store on the mobile device by tapping
the button several times at specific points in the sequence.
Modal controls are very effective for operations that should be
confirmed by users. They can also be excellent triggers when one of a
set of options is available, depending on context. For example, the
individual record screen in the Contacts application allows you to click an Edit button that
changes the view to an editable view of the record. The Edit button then
becomes a Done button that can be used to cancel or commit any editing
actions that have taken place. Figure 3 shows
the modal control in the Contacts
application.
Providing secondary confirmations is a common and useful pattern
for preventing destructive
operations such as deletions.
2.1. Creating a modal button subclass
This example illustrates just one way to create a modal button.
There are several options, including building custom
UIView objects from scratch. I chose to take a
simpler route and simply subclassed UIButton in
order to keep the interface consistent, reduce duplication of code,
and avoid tiny deviations in the look and feel of my button and
standard UIButton instances. The implementation is
fairly standard. ModalButton is a subclass of
UIButton and overloads the touchesEnded:withEvent: method to add
custom handling behavior. Specifically, each touch deactivates the
button and sets an NSTimer to fire after a delay,
presenting the next state in a finite sequence of modes. The delay
emulates what might happen if you were to design the App Store button
that triggers an application download over HTTP and acts as an
implicit opt-in agreement to purchase an application (versus a
one-touch sequence, which might be triggered by accident).
The ModalButton class adheres to the
UIControl target-action messaging mechanism. In
this example, the main view controller, ModalButtonViewController, adds
itself as a target for the UIControlEventTouchUpInside control
event. Doing so sends a specific action message to the
ModalButtonViewController instance when a
UITouch sequence begins and ends inside the bounds
of the ModalButton:
#import "ModalButtonViewController.h"
@implementation ModalButtonViewController
- (void)viewDidLoad
{
[super viewDidLoad];
button = [[ModalButton alloc] init];
[button addTarget:self action:@selector(performModalActionForButton:)
forControlEvents:UIControlEventTouchUpInside];
[button retain];
[self.view addSubview:button];
button.center = CGPointMake(160.0, 210.0);
}
- (void)performModalActionForButton:(id)sender
{
ModalButton *btn = (ModalButton *)sender;
ModalButtonMode mode = btn.mode;
NSLog(@"The button mode is: %d", mode);
}
- (void)dealloc
{
[button release];
[super dealloc];
}
@end
The definition of the ModalButton class
follows:
#import <UIKit/UIKit.h>
static NSString *download = @"Download";
static NSString *downloading = @"Downloading...";
static NSString *install = @"Install";
static NSString *installing = @"Installing...";
static NSString *complete = @"Complete!";
static UIImage *heart = nil;
static UIImage *clover = nil;
typedef enum {
ModalButtonModeDefault = 0,
ModalButtonModeDownload,
ModalButtonModeInstall,
ModalButtonModeComplete,
} ModalButtonMode;
@interface ModalButton : UIButton {
ModalButtonMode mode;
NSTimer *timer;
UIActivityIndicatorView *indicator;
}
@property (readonly) ModalButtonMode mode;
- (void)update:(NSTimer *)theTimer;
@end
#import "ModalButton.h"
@interface ModalButton (PrivateMethods)
- (void)handleTap;
@end
@implementation ModalButton
@synthesize mode;
- (id) init
{
self = [self initWithFrame:CGRectMake(0.0, 0.0, 118.0, 118.0)];
return self;
}
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
mode = ModalButtonModeDefault;
heart = [UIImage imageNamed:@"heart.png"];
clover = [UIImage imageNamed:@"clover.png"];
indicator = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
indicator.hidesWhenStopped = YES;
[self addSubview:indicator];
[self bringSubviewToFront:indicator];
indicator.center = self.center;
self.backgroundColor = [UIColor clearColor];
[self setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal];
[self setTitleColor:[UIColor whiteColor] forState:UIControlStateDisabled];
self.titleEdgeInsets = UIEdgeInsetsMake(-90.0, 0, 0, 0);
self.font = [UIFont fontWithName:@"Helvetica" size:12.0f];
[self setBackgroundImage:heart forState:UIControlStateNormal];
[self update:nil];
}
return self;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
if([self hitTest:[touch locationInView:self] withEvent:event]){
[self handleTap];
}
[super touchesEnded:touches withEvent:event];
}
- (void)handleTap
{
self.enabled = NO;
NSString *title;
switch(mode){
case ModalButtonModeDownload:
title = downloading;
break;
case ModalButtonModeInstall:
title = installing;
break;
default:
break;
}
[self setTitle:title forState:UIControlStateNormal];
if([timer isValid]){
[timer invalidate];
}
timer = [NSTimer timerWithTimeInterval:5.0 target:self
selector:@selector(update:)
userInfo:nil repeats:NO];
[timer retain];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[indicator startAnimating];
}
- (void)update:(NSTimer *)theTimer
{
NSString *title;
// Toggle mode
switch(mode){
case ModalButtonModeDefault:
mode = ModalButtonModeDownload;
title = download;
self.enabled = YES;
break;
case ModalButtonModeDownload:
mode = ModalButtonModeInstall;
title = install;
self.enabled = YES;
break;
case ModalButtonModeInstall:
mode = ModalButtonModeComplete;
title = complete;
[self setBackgroundImage:clover forState:UIControlStateNormal];
self.enabled = NO;
break;
default:
self.enabled = NO;
return;
}
[self setTitle:title forState:UIControlStateNormal];
if([timer isValid]){
[timer invalidate];
}
[indicator stopAnimating];
}
- (void)dealloc
{
if([timer isValid]){
[timer invalidate];
}
[timer release];
[indicator release];
[super dealloc];
}
@end