Search
Follow
Recent Comments

Entries in airprint (3)

Friday
Aug262011

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];
    }
  }

}

 

As always the example code for this post can be found here or in my CodeExamples git hub repository.

Monday
Aug082011

Printing Headers and Footers with a Print Page Renderer

I previously covered how to add basic printing with AirPrint to an iOS app. In this post I will show how you can customise the print output by adding a custom Print Page Renderer. This is the approach you will need to take if you want to add a header and footer to the printed page.

Page Renderers

As I showed in the previous post the main object to control a print job within an iOS app is the shared instance of the UIPrintInteractionController. You can control basic print formatting options by using a print formatter which can be obtained directly from common UIView classes such as a UIWebView or UITextView. However if you want to customise the print layout or add things such as headers and footers you need to use a UIPrintPageRenderer class. The print page renderer object can have one or more print formatters associated with it, each formatter controlling the format of specific pages or ranges of pages.

In the basic printing example we obtained a UIViewPrintFormatter from the UIWebVIew and assigned it to the printFormatter property of the UIPrintInteractionController object:

  UIViewPrintFormatter *formatter = [self.webView viewPrintFormatter];
  pc.printFormatter = formatter;

When using a print page renderer you should no longer set the print formatter directly on the UIPrintInteractionController. Instead you assign one or more print formatters to the printFormatters property, an NSArray, of the UIPrintPageRenderer object. The print page renderer object is then assigned to the printPageRenderer property of the UIPrintInteractionController.

If all you want to do is use multiple print formatters to allow individual control of the format of specific pages in a print job you can use an instance of UIPrintPageRenderer directly. However greater control is possible if you subclass the UIPrintPageRenderer and override one or more of the following methods:

  • drawHeaderForPageAtIndex:inRect: override this method to draw the header for a page. For this method to be called you must ensure you set the headerHeight property in the subclassed UIPrintPageRenderer object.
  • drawFooterForPageAtIndex:inRect: override this method to draw the footer for a page. For this method to be called you must ensure you set the footerHeight property in the subclassed UIPrintPageRenderer object.
  • drawContentForPageAtIndex:inRect: override this method to draw custom content for the page.
  • drawPrintFormatterLforPageAtIndex: override this method to allow custom drawing such as an overlay to be mixed with the print formatter assigned to a specific page.

Creating a Generic Print Page Renderer

There are many apps with printing requirements which can be met with a basic print formatter combined with a print page renderer to add custom headers and footers. I have therefore tried to create a failry generic subclass of UIPrintPageRenderer that can be reused by many iOS apps. The interface for the subclass is defined as follows:

@interface UYLGenericPrintPageRenderer : UIPrintPageRenderer;

 

@property (nonatomic, copy) NSString *headerText;

@property (nonatomic, copy) NSString *footerText;


The subclass is named UYLGenericPrintPageRenderer to hopefully avoid conflicting with any classes that Apple may add to UIKit. The interface is very simple in that I have for now added just two optional properties to specify the text for the header and footer. I have also set a number of defaults for the font and padding sizes that will be used when drawing the headers and footers:

#define HEADER_FONT_SIZE 14

#define HEADER_TOP_PADDING 5

#define HEADER_BOTTOM_PADDING 10

#define HEADER_RIGHT_PADDING 5

#define HEADER_LEFT_PADDING 5

 

#define FOOTER_FONT_SIZE 12

#define FOOTER_TOP_PADDING 10

#define FOOTER_BOTTOM_PADDING 5

#define FOOTER_RIGHT_PADDING 5

#define FOOTER_LEFT_PADDING 5


For greater flexibility these could easily be defined as additional properties allowing them to be changed for each print job. I allow the compiler to synthesise the getter methods for the two properties but I am going to define my own setters for the header and footer text. This allows me to calculate the headerHeight and footerHeight properties of the UIPrintPageRenderer at the same time. The setter for the headerText property is shown below:

- (void)setHeaderText:(NSString *)newString {

 

  if (_headerText != newString) {
    [_headerText release];
    _headerText = [newString copy];

 

    if (_headerText) {
      UIFont *font = [UIFont fontWithName:@"Helvetica" size:HEADER_FONT_SIZE];
      CGSize size = [_headerText sizeWithFont:font];
      self.headerHeight = size.height + HEADER_TOP_PADDING + HEADER_BOTTOM_PADDING;
    }
  }

}

 

This is very similar to a normal NSString copy setter method with the addition that we calculate the size of the header whenever we set the header text. We make the assumption that the header will always use the Helvetica font and we rely on the predefined values for the font size and top and bottom padding. I will not show the setter for the footerText as it is very similar and you can take a look for yourself in the example code.

Drawing the Header

To draw the header for a page we just need to override the drawHeaderForPageAtIndex:inRect: method in our print page renderer subclass. Since the method includes the page index you could draw different header content based on the page but in this case we simply output the header text as follows:

- (void)drawHeaderForPageAtIndex:(NSInteger)pageIndex inRect:(CGRect)headerRect {

 

  if (self.headerText) {
    UIFont *font = [UIFont fontWithName:@"Helvetica" size:HEADER_FONT_SIZE];
    CGSize size = [self.headerText sizeWithFont:font];

 

    // Center Text
    CGFloat drawX = CGRectGetMaxX(headerRect)/2 - size.width/2;
    CGFloat drawY = CGRectGetMaxY(headerRect) - size.height - HEADER_BOTTOM_PADDING;
    CGPoint drawPoint = CGPointMake(drawX, drawY);
    [self.headerText drawAtPoint:drawPoint withFont:font];
  }

}

 

This is hopefully fairly straightforward, the headerRect passed into the method specifies the rectangle into which we should draw the header. In this case I have chosen to simply center the text in the middle of the header.

Drawing the Footer

Drawing the footer is similar to drawing the header except this time we override the drawFooterForPageAtIndex:inRect: method. The code is shown below:

- (void)drawFooterForPageAtIndex:(NSInteger)pageIndex inRect:(CGRect)footerRect {

 

  UIFont *font = [UIFont fontWithName:@"Helvetica" size:FOOTER_FONT_SIZE];

 

  NSString *pageNumber = [NSString stringWithFormat:@"- %d -", pageIndex+1];
  CGSize size = [pageNumber sizeWithFont:font];
  CGFloat drawX = CGRectGetMaxX(footerRect)/2 - size.width/2;
  CGFloat drawY = CGRectGetMaxY(footerRect) - size.height - FOOTER_BOTTOM_PADDING;
  CGPoint drawPoint = CGPointMake(drawX, drawY);
  [pageNumber drawAtPoint:drawPoint withFont:font];

 

  if (self.footerText) {
    size = [self.footerText sizeWithFont:font];
    drawX = CGRectGetMaxX(footerRect) - size.width - FOOTER_RIGHT_PADDING;
    drawPoint = CGPointMake(drawX, drawY);
    [self.footerText drawAtPoint:drawPoint withFont:font];
  }

}

 

For the footer I use the pageIndex parameter to write the page number in the center of the footer and I add the footer text, if set, aligned to the right of the footer.

Putting it All Together

With the custom print page renderer subclass defined we just need to change our web view controller printWebView method to create an instance of the page renderer. I will not show the whole method again as the only piece that has changed is as follows:

UYLGenericPrintPageRenderer *renderer = [[UYLGenericPrintPageRenderer alloc] init];

renderer.headerText = printInfo.jobName;

renderer.footerText = @”AirPrinter - UseYourLoaf.com”;

 

UIViewPrintFormatter *formatter = [self.webView viewPrintFormatter];

[renderer addPrintFormatter:formatter startingAtPageAtIndex:0];

pc.printPageRenderer = renderer;

[renderer release];

After creating an instance of our custom print page renderer object we set the header and footer text and then we add the print formatter for the web view to our renderer object. Note that when you add the formatter you can specify the starting page that the formatter should be applied to. This allows different formatters to be specified for different ranges of pages in the content. In this case we use the same formatter for the whole print job. Finally we set the printPageRenderer property of the UIPrintInteractionController object.

Wrapping Up

I hope these two posts on printing with iOS have shown how easy it is to add print capabilities to an iOS app. The use of a custom print page renderer increases the control you have over the format of the print output. Defining a generic print page renderer subclass which can be reused in many different iOS projects also makes it easy to quickly add print support to an App. As always you can find the source code for the example here or in my coding examples repository on GitHub.

Thursday
Aug042011

Basic Printing with AirPrint

Apple introduced the ability to print wirelessly with iOS 4.2 using AirPrint enabled printers. Adding basic printing functionality to an iOS app is pretty straightforward, requiring just a few lines of code. In this post I will walk through adding basic printing support to an app, this will lay the foundation for some more advanced printing in a follow up post.

One caveat is that printing is only supported on devices which support multitasking which excludes older devices such as the iPhone 3G.

Printing a Web Page

To illustrate how to add support for printing I have created a simple app that allows you to enter a URL which is then displayed in a UIWebView. I have made it a universal app so we can look at the minor differences between the iPad and iPhone/iPod touch user interfaces. I will not cover all of the code to create this app as it is very simple and you can take a look at the example code yourself. The app is created from the Navigation based app template and consists of a root view controller which displays a UITextField to allow the entry of a URL as shown below:

The UITextField delegate method in the root view controller is pretty simple as shown below. It extracts the string from the UITextField and passes it to a new instance of a view controller that contains the UIWebView. Note that for brevity there is no validation that the text string actually contains a valid URL. The new view controller is then pushed onto the navigation controller stack.

- (BOOL)textFieldShouldReturn:(UITextField *)textField {

 

  if (textField.text) {
    WebViewController *viewController = [[WebViewController alloc] initWithNibName:@"WebViewController" bundle:nil];
    viewController.query = textField.text;
    [[self navigationController] pushViewController:viewController animated:YES];
    [viewController release];
  }
  [textField resignFirstResponder];
  return YES;

}

 

The web view controller uses NSURLRequest to retrieve the contents of the URL and display it in a UIWebView. This is mostly achieved in the viewDidLoad method:

- (void)viewDidLoad

{

  [super viewDidLoad];
  self.title = self.query;

 

  NSString *escapedQuery = [self.query stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
  NSURL *url = [[NSURL alloc] initWithString:escapedQuery];

  if (url) {

 

    NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
    [self.webView loadRequest:request];

 

    [request release];
    [url release];
  }

}

 

The web view is shown below (it looks pretty much the same on the iPhone and the iPad):

Adding a Print Button

The first thing we will do is add a button to the navigation bar that can be used by the user to trigger the print user interface. We can do this in the viewDidLoad method of the web view controller as follows (previous code omitted):

- (void)viewDidLoad

{

  // some code omitted

 

  if ([UIPrintInteractionController isPrintingAvailable]) {
    UIBarButtonItem *barButton = [[UIBarButtonItem alloc] 
                                   initWithBarButtonSystemItem:UIBarButtonSystemItemAction
                                                       target:self
                                                       action:@selector(printWebView:)];

 

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

}

 

Some points to note about this code:

  • Before adding the printer button we first check if the device is able to support printing using the isPrintingAvailable class method of UIPrintInteractionController.
  • The UIPrintInteractionController class along with the other print classes we need are part of UIKit so there is no need to link additional frameworks to support printing.
  • The UIBarButtonSystemItemAction button is recommended by Apple though there is of course nothing stopping you from using a custom button if you feel the need.

Simple Printing

Before getting too clever I will show the quick and easy way to print the contents of a UIView (in this case it is a UIWebView but the technique works for all UIKit views). To print we need to implement the printWebView method which is the target of the print button. I’ll step through this a few lines at a time. The first thing to do is get the shared instance of the UIPrintInteractionController object:

- (void)printWebView:(id)sender {

 

  UIPrintInteractionController *pc = [UIPrintInteractionController sharedPrintController];

 

The UIPrintInteractionController is the key controlling class for interacting with the iOS printing system. It reperesents a print job so as well as the item(s) to be printed it can contain the following:

  • Print Job Options: selected printer ID, jobName, orientation, duplex, outputType
  • Print Formatting Options: start page, page count, content insets, width and height
  • Print Page Renderers: headers, footers, custom print page layouts

The print job options are stored in an object of class UIPrintInfo. In this example I am going to set two options: the job name and the outputType. There are three possible values for the output type depending on what you are printing:

  • UIPrintInfoOutputGeneral: This is the default and should be used if you are printing a mix of text, graphics and images.
  • UIPrintInfoOutputPhoto: As the name suggests this is for printing photos. It assumes a photo sized print page such as 4x6 or A6.
  • UIPrintInfoOutputGrayscale: Used when you know the content consists of just black text.

Since in this case we are printing web pages which can be a mix of text and graphics we will use UIPrintInfoOutputGeneral and set the job name to the URL string:

  UIPrintInfo *printInfo = [UIPrintInfo printInfo];
  printInfo.outputType = UIPrintInfoOutputGeneral;
  printInfo.jobName = self.query;
  pc.printInfo = printInfo;

If you are printing something that might contain multiple pages you can allow the user to select which pages to print by showing the page range option:

  pc.showsPageRange = YES;

You control the layout of a printed page using a print formatter to specify margins, which page to start printing from, etc. If you are printing a UIWebView, UITextView or MKMapView you can obtain a print formatter directly from the view using the viewPrintFormatter method:

  UIViewPrintFormatter *formatter = [self.webView viewPrintFormatter];
  pc.printFormatter = formatter;

The next step is optional but I think is worth doing as it will help you spot errors during development. To get back error information for a print job you need to declare a block which acts as a completion handler. The block is invoked when the print job completes so in this case I just use it to log any errors when compiling in debug mode:

  UIPrintInteractionCompletionHandler completionHandler = 
  ^(UIPrintInteractionController *printController, BOOL completed, NSError *error) {
    if(!completed && error){
      DLog(@"Print failed - domain: %@ error code %u", error.domain, error.code); 
    }
  };

I have posted previously on how to conditionally compile for debug builds if you are interested in the definition of DLog.

Finally we need to present the user interface for the print system to allow the user to select the printer, override any of the default options and finally print the content. This is about the only point where we need to do something different for the iPhone and iPad. In the case of the iPhone the printing options are presented to the user with a modal sheet that slides up from the bottom of the screen. In the case of the iPad you can either present a popover view from the print button on the toolbar or from some arbitrary rectangle in the view. The UI_USER_INTERFACE_IDIOM function makes it easy to implement this device specific code:

  if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
    [pc presentFromBarButtonItem:self.printButton animated:YES completionHandler:completionHandler];
  } else {
    [pc presentAnimated:YES completionHandler:completionHandler];
  }

You can see the difference between the iPhone modal sheet and the iPad popover view in the following two screenshots:

Testing using the Printer Simulator

If you had to print to a real printer each time you wanted to test printing from an application you could waste a lot of ink and paper (assuming you have an AirPrint compatibile printer). Luckily the iOS SDK ships with a printer simulator which display the printed pages using Preview. The easiest way to activate the print simulator is to run your app in the iOS Simulator and then select “Open Print Simulator” from the File menu. With the print simulator running on a Mac you can also print from an iOS device that is on the same network.

With the print simulator running, tapping the print button should generate the printed output in preview. The output looks the same as the contents as the web view without any page numbering, headers, footers or other custom layout. For the small amount of code that we have added to the App this is not bad. To get a little more fancy we will need to add a Print Page Renderer.

Wrapping Up

Since this is turning into a long post I will save the details on how to use a custom Print Page Renderer until the next post. You can find the code for this project (including the page renderer if you want to take a sneak peek at what is coming up) here or in my coding examples repository on GitHub.