Using UIActionSheet for external actions

In two recent posts I walked through basic printing with AirPrint and how to print headers and footers with a print page renderer. In both cases though I assumed that the print user interface was accessed directly from the system action button on the toolbar. The user interface is shown below for the iPhone and iPad versions:

Touching the system action button brings up the print interaction controller view which is a modal dialog on the iPhone but a popover style window on the iPad. This works fine if the only external action you want to perform is to print. If you want to add options such as opening content in a web browser, sharing with external services such as twitter and Facebook or perhaps emailing content you need to present a menu of options. The desired user interface is shown below again for both the iPhone and iPad versions:

In both cases the menu of options is presented using a UIActionSheet but there are some subtle differences between the iPhone and iPad implementations that I will cover in detail in this post. Before we get into the code it is worth pointing out the differences in the way a user interacts with the menu on the different devices:

  • on iPhone devices the action sheet is presented as a modal view which slides in from the bottom of the screen when the action button is touched. Since this is a modal interface the user cannot interact with the remaining parts of the user interface which are dimmed and partly covered by the action sheet. The only way for the user to exit from the dialog is to select one of the options or use the cancel button.
  • on iPad devices the action sheet is presented in a popover style dialog with a pointer back to the system action button on the toolbar. This is not a modal interface so the user can still interact with other user interface elements. According to Apple user interface guidelines touching outside of the popover should cancel and remove the popover. This also means that if the user touches the button again this should be treated as cancelling the popover. For this reason the popover menu never includes a cancel button.
  • when the print action is selected the action sheet should be dismissed and the printer options dialog view displayed in its place. As we have previously seen this is a modal view on the iPhone and a popover view on the iPad.

Now we know what the target interface looks like we will start to modify the AirPrint example app from the earlier posts to add a menu of options.

Keeping Track of Visible Views

As I have already mentioned we can use a UIActionSheet to show the available actions. On the iPhone where everything is modal things are pretty simple. On the iPad things can get a little more tricky for the unwary. The key to implementing this interface on the iPad is to keep track of when we have the action sheet or the print interaction controller views visible. For a UIActionSheet that is easy since it has a property that will tells us exactly that:

@property(nonatomic, readonly, getter=isVisible) BOOL visible

For the printer view we will need to add one to our view controller and update it ourselves. The revised interface definition for the WebViewController class is shown below:

#import <UIKit/UIKit.h>
#import <MessageUI/MFMailComposeViewController.h>

@interface WebViewController : UIViewController <UIWebViewDelegate,
                                             UIActionSheetDelegate,
                              UIPrintInteractionControllerDelegate,
                               MFMailComposeViewControllerDelegate>

@property (nonatomic, copy) NSString *query;
@property (nonatomic, retain) IBOutlet UIWebView *webView;
@property (nonatomic, retain) UIBarButtonItem *actionButton;
@property (nonatomic, retain) UIActionSheet *actionSheet;
@property (nonatomic, assign, getter = isPicVisible) BOOL picVisible;
@end

The changes from the original version are as follows:

  • we need to implement a number of delegate methods for the UIActionSheet, print interaction controller and the mail compose view to allow us to interact with and dismiss the various views. I will cover these in more detail as we step through the implementation.
  • I have renamed the UIBarButtonItem from “printButton” to “actionButton” since it now has a more generic purpose.
  • The actionSheet property is used to keep track of the UIActionSheet that we will create and then reuse as required.
  • the picVisible property is a boolean flag that we will use to keep track of when the print interaction controller is visible. Note that we use a custom getter name (isPicVisible) for readability.

For brevity I will not show the synthesise statements for these properties and the other memory management code in viewDidUnload and dealloc. You can find these in the example code if you are interested. Also note that previously we only showed the system action button if the device supported printing whereas now we always want to show it in the toolbar. The modified code in viewDidLoad to setup the button is as follows:

- (void)viewDidLoad {
  ...
  UIBarButtonItem *barButton = [[UIBarButtonItem alloc]
                   initWithBarButtonSystemItem:UIBarButtonSystemItemAction
                   target:self
                   action:@selector(performAction:)];

  [self.navigationItem setRightBarButtonItem:barButton animated:NO];
  self.actionButton = barButton;
  [barButton release];

  self.picVisible = NO;
}

When the button is touched we will get a call to our performAction: method which we will look at shortly but first I want to cover how we track whether the print interaction controller view is visible. This requires a minor change to the existing printWebView method which is responsible for presenting the printer options view. I say minor because all we need to do is set the delegate and then implement two methods from the UIPrintInteractionControllerDelegate protocol:

- (void)printWebView {
  UIPrintInteractionController *pc = [UIPrintInteractionController
                                      sharedPrintController];
  pc.delegate = self;
  ...
}

- (void)printInteractionControllerDidPresentPrinterOptions:
        (UIPrintInteractionController *)printInteractionController {
  self.picVisible = YES;
}

- (void)printInteractionControllerDidDismissPrinterOptions:
        (UIPrintInteractionController *)printInteractionController {
  self.picVisible = NO;
}

As the printer options dialog is presented and dismissed we can use the corresponding DidPresent and DidDismiss delegate methods to set and clear the picVisible property allowing us to keep track of the dialog state. Note that there is a situation where the DidDismiss delegate method is not called that we will cover later.

Showing the Action Sheet

With the basic groundwork done we can move on to actually displaying the UIActionSheet when our system action button is touched. I will lazily create the action sheet the first time we access the actionSheet property by providing my own custom getter method as follows:

- (UIActionSheet *)actionSheet {

  if (_actionSheet == nil) {

    NSString *cancelButtonTitle = @"Cancel";
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
      cancelButtonTitle = nil;
    }

    if ([UIPrintInteractionController isPrintingAvailable]) {
      _actionSheet = [[UIActionSheet alloc]
                      initWithTitle:nil
                           delegate:self
                  cancelButtonTitle:cancelButtonTitle
             destructiveButtonTitle:nil
                  otherButtonTitles:@"Open in Safari",
                                    @"E-mail link",
                                    @"Print", nil];
    } else {
      _actionSheet = [[UIActionSheet alloc]
                      initWithTitle:nil
                           delegate:self
                  cancelButtonTitle:cancelButtonTitle
             destructiveButtonTitle:nil
                  otherButtonTitles:@"Open in Safari",
                                    @"E-mail link", nil];
    }
  } 

  return _actionSheet;
}

The getter method first checks the ivar (_actionSheet) to determine if we already have a UIActionSheet object allocated and if true simply returns it. The UIActionSheet we create is the same for both the iPhone and iPad devices with one exception - we do not need a cancel button on the iPad. We use UI_USER_INTERFACE_IDIOM() to test for the iPad and adjust the cancel button title as required.`

The options that we will show on the action sheet are specified in the otherButtonTitles parameter of the UIActionSheet initialiser. In practise you would probably want to localise the actual string values. To ensure we only show the print action on devices that actually support printing we first test if printing is available using the class method of UIPrintInteractionController. Since the order will be important when we come to respond to the user touching a button I have placed the optional print button last.

To actually get the action sheet on screen we need to implement the performAction method which is the target action of the system action button we added to the toolbar in the viewDidLoad method. As we will see the performAction method needs to do a little bit more than just present the action sheet when the button is touched:

- (void)performAction:(id)sender {

  if ([self.actionSheet isVisible]) {
    [self.actionSheet dismissWithClickedButtonIndex:-1 animated:NO]; 

  } else if ([self isPicVisible]) {
    UIPrintInteractionController *pc = [UIPrintInteractionController
                                        sharedPrintController];
    [pc dismissAnimated:YES];
    self.picVisible = NO;

  } else {

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
      [self.actionSheet showFromBarButtonItem:self.actionButton
                                     animated:NO];

    } else {
      [self.actionSheet showInView:[self view]];
    }
  }
}

Note that the reference to self.actionSheet invokes our previously defined actionSheet getter method which the first time it is called will allocate a UIActionSheet object for us.

A common mistake when you first start to use UIActionSheet on the iPad is to allocate and present a new action sheet each time the toolbar button is touched. This mistake is easy to make if you are used to presenting a UIActionSheet modally on the iPhone where you do not need to worry about dismissing the view yourself. The interesting effect of this approach is that it may not at first be obvious that anything is wrong. The action sheet is displayed and functions correctly and when you select an option or click outside the popover window it is dismissed as expected. If however you touch the toolbar button twice you can just about notice that the shadow around the popover window darkens as a new action sheet is created and presented over the top of the existing sheet. You get a further clue that something is wrong when it takes two touches outside the popover window to dismiss both of the action sheets.

As I mentioned when discussing the user interface at the start of this post when the iPad toolbar system action button is touched the action sheet, if it is already visible, should be dismissed. So the first thing we do is check if the action sheet is visible and dismiss it if it is. You can dismiss an action sheet using the dismissWithClickedButtonIndex:animated method. Since in this case we do not want to specify an action we pass a button index (-1) that we are sure does not correspond to a valid button.

We perform a similar test for the printer options window using the picVisible property we previously defined to keep track of when the print interaction controller view is displayed. One potential pitfall here is that when we call dismissAnimated on the shared print controller to dismiss the printer options it appears that the corresponding delegate method (printInteractionControllerDidDismissPrinterOptions) is not called. To ensure we keep track of when the printer options are visible we need to clear the boolean picVisible property at this point rather than relying on the call to the delegate method to do it for us.

Finally if we are sure that we are not dismissing either the action sheet or the printer options we can present the action sheet to the user. The way we present the action sheet depends on whether we are running on the iPhone or iPad so we again use UI_USER_INTERFACE_IDIOM to select the correct method. For the iPad the action sheet is shown using showFromBarButtonItem:animated so that it originates from the action button on the toolbar. On the iPhone the showInView method is used and the view is presented modally sliding up from the bottom.

Handling Action Sheet Delegate Methods

When the UIActionSheet is created the delegate is set to the current view controller (self) which must then implement actionSheet:clickedButtonAtIndex: to process the buttons we added to the sheet. The implementation of this method in our WebViewController is simple:

- (void)actionSheet:(UIActionSheet *)actionSheet
        clickedButtonAtIndex:(NSInteger)buttonIndex {

  switch (buttonIndex) {
    case 0:
      [self openInBrowser];
      break; 

    case 1:
      [self openInEmail];
      break;

    case 2:
      [self printWebView];
      break;

    default:
      break;
  }
}

Depending on which button was touched (if any) we invoke the appropriate action method to open the link in an external browser, open an email message containing the link or to print the web view. We have already seen the printWebView method in the previous posts and the openInBrowser and openInEmail methods are trivial so I will not show the details here (refer to the example code if you are interested).

Finishing Up

There is one final iPad related issue we need to take care of before we are done which is to handle dismissing the popover window when the WebViewController view disappears. Since on the iPad the UIActionSheet is presented as a popover rather than modally there is nothing to stop the user touching the back button to return to the home screen. In that situation we need to make sure we remove the popover window from the screen. The viewWillDisappear method already contains some code to dismiss the printer options window so we can make a small modification to also check for and dismiss the action sheet as follows:

- (void)viewWillDisappear:(BOOL)animated {
  ...

  if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {

    if ([self isPicVisible]) {
      UIPrintInteractionController *pc = [UIPrintInteractionController
                                          sharedPrintController];
      [pc dismissAnimated:animated];
      self.picVisible = NO;
    }

    if ([self.actionSheet isVisible]) {
      [self.actionSheet dismissWithClickedButtonIndex:-1 animated:NO];
    }
  }
}

The example Xcode project for this post is now very out-dated but you can find it archived in my code examples repository: