As time has progressed and technology has developed, one of the clearest trends to be noticed is the growth in user-driven content. With the improvement of design technologies, Internet connection speeds, and network availability, the amount of data generated electronically per year has increased at a nearly unbelievable rate. The heart of this matter is based around the idea of allowing users to easily take in and re-distribute information. You can incorporate these same concepts into your development through a variety of built-in classes in order to improve the functionality and usefulness of your applications.
In this chapter, you will need only a physical device to implement texting functionality, which you will build in your first recipe. All your other functionalities will be able to be simulated.
Text messaging is quite easily one of the currently most incredibly popular methods of transmitting data between individuals. It’s quick, easy, and powerful, and is used across nearly all age groups. In iOS you can actually incorporate text messaging into your applications in order to easily provide your users with the simple cross-application functionality that can so easily improve the overall quality of an application.
Start off by creating a new project called “SendItOut”, which you will use throughout this chapter, with a class prefix “Main”. Select the Single View Application template to create a simple project, as in Figure 13–1.
In order to fully demonstrate a few of the functionalities of this topic, you will specifically choose to develop your application for the iPad, rather than the iPhone. Make sure the Device Family is set accordingly in the next screen after entering the project's name. Since some of the functionalities you will test require a physical device to be fully capable, you can make this application for the iPhone as well, and simply adjust the view elements as you wish. Configure your project with the name “SendItOut”, class prefix “Main”, and make sure the Use Automatic Reference Counting box is checked, as in Figure 13–2.
After clicking through to create your project, switch over to your view controller's XIB file. In the utilities pane, set the orientation (under the Simulated Metrics section) to landscape, and make sure that the background color is set to gray.
You will start off by adding a UITextView
, with the default Lorem Ipsum text, to the top half of your view, as well as a UIButton
, with the label “Text Message”, to the bottom. Connect these to your view controller with property names textViewInput
and textButton
, and connect the button to an IBAction
,-textPressed:
.
Before you proceed, you will go ahead and round the corners of your UITextView
to improve your application's visual quality. Add the following import statement to your view controller's header file.
#import <QuartzCore/QuartzCore.h>
Next add the following line to the end of your -viewDidLoad
method.
self.textViewInput.layer.cornerRadius = 15.0;
Your view should now resemble that simulated in Figure 13–3 once you rotate your simulator. This can be done through either the “Rotate Left” (+left) or “Rotate Right” (+right) commands found in the Hardware menu of the iOS simulator.
Next, you will configure your view controller as the delegate for your UITextView
. Add the following line to your -viewDidLoad
method.
self.textViewInput.delegate = self;
Next, you will add the MessageUI framework to your project. Do this under the Build Phases tab of your project as usual, and add the required import statement to your view controller's header file.
#import <MessageUI/MessageUI.h>
You need to instruct your view controller to conform to certain protocols. Adjust your header file so that the UITextViewDelegate
, MFMessageComposeViewControllerDelegate
, and UINavigationControllerDelegate
are all conformed to.
At this point, your fully set-up header file will now read like so:
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#import <MessageUI/MessageUI.h>
@interface MainViewController : UIViewController <UITextViewDelegate, MFMessageComposeViewControllerDelegate, UINavigationControllerDelegate>
@property (strong, nonatomic) IBOutlet UITextView *textViewInput;
@property (strong, nonatomic) IBOutlet UIButton *textButton;
-(IBAction)textPressed:(id)sender;
@end
Now you will implement one of your UITextView
's delegate methods in order to ensure that your keyboard is properly dismissed when the user taps the Enter key.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range
replacementText:(NSString *)text
{
if ([text isEqualToString:@"
"])
{
[textView resignFirstResponder];
return FALSE;
}
return TRUE;
}
Now you can implement your -textPressed:
method such that the text of your UITextView
is transposed into a text message. You will simply set the recipient to a fake number.
-(void)textPressed:(id)sender
{
if ([MFMessageComposeViewController canSendText])
{
MFMessageComposeViewController *messageVC = [[MFMessageComposeViewController alloc] init];
messageVC.messageComposeDelegate = self;
messageVC.recipients = [NSArray arrayWithObject:@"3015555309"];
messageVC.body = self.textViewInput.text;
[self presentModalViewController:messageVC animated:YES];
}
else
{
NSLog(@"Error, Text Messaging Unavailable");
}
}
The implementation of this method should appear fairly straightforward. After using the +canSendText
method to check for texting availability, you created an instance of the MFMessageComposeViewController
class, and then configured it with your fake recipient, as well as the intended text. Finally, you simply present the controller modally to allow your user to review the text message before sending it.
The MFMessageComposeViewController
andits counterpart you will encounter later, the MFMailComposeViewController
, are both classes that allow you to set their initial conditions and present them, but they do not allow you any control of the class once it has been shown. This is to ensure that the user has the final say in whether a message or mail sends, rather than any application sending it without informing the user.
You can implement your MFMessageComposeViewController
's messageComposeDelegate
method to handle the completion of the message like so:
-(void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result
{
if (result == MessageComposeResultSent)
{
self.textViewInput.text = @"Message sent.";
}
else if (result == MessageComposeResultFailed)
{
NSLog(@"Message Failed to Send!");
}
[self dismissModalViewControllerAnimated:YES];
}
Along with the two possible values of MessageComposeResults
demonstrated in the previous code, there is a third result,MessageComposeResultCancelled
, which indicates that the user cancelled the sending of the text message.
A new functionality in iOS 5.0 is the ability to receive notifications about the changing of the availability of text messaging. You can register for such notifications by adding the following line to the -viewDidLoad
method:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(availabilityChange:)
name:@"MFMessageComposeViewControllerTextMessageAvailabilityDidChangeNotification"
object:nil];
The selector specified here can easily be defined to simply inform you of the change. In a full application, you might likely make use of a UIAlert
to notify the user of this change as well, but you will avoid this process for demonstration purposes.
-(void)availabilityChange:(id)sender
{
if ([MFMessageComposeViewController canSendText])
{
NSLog(@"Text Messaging Available");
}
else
{
NSLog(@"Text Messaging Unavailable");
}
}
Your application can now copy the body of your UITextView
into a text message to be sent off to your fake recipient! If you test this, however, keep in mind that the text messaging functionality will not be available on the iOS simulator. You will have to test this on your physical device with 3G capabilities. To test this application as it is exactly, you will need a 3G-capable iPad, but you could edit the project to work for an iPhone instead.
Just as you were able to create and configure text messages to be sent from your application, you can also use the MessageUI framework that you dealt with in the previous recipe to configure mail messages using the counterpart to the MFMessageComposeViewController
class, which is known as MFMailComposeViewController
.
Building upon your set-up application, you will add another button with the label “Mail”. Use the property name mailButton
, and assign it an action called -mailPressed:
.
The setup for your -mailPressed:
method is very similar to your previous -textPressed:
method. You will create your composing view controller, configure it, and then present it.
-(void)mailPressed:(id)sender
{
if ([MFMailComposeViewController canSendMail])
{
MFMailComposeViewController *mailVC = [[MFMailComposeViewController alloc] init];
[mailVC setSubject:@"SendItOut"];
[mailVC setToRecipients:[NSArray arrayWithObject:@"[email protected]"]];
[mailVC setMessageBody:self.textViewInput.text isHTML:NO];
mailVC.mailComposeDelegate = self;
[self presentModalViewController:mailVC animated:YES];
}
else
{
NSLog(@"Error: Mail Unavailable");
}
}
As you can see, the MFMailComposeViewController
has a few extra properties compared to the MFMessageComposeViewController
to specifically configure a more complex e-mail.
Since you set your mailComposeDelegate
property to your view controller, you will need to specify that it will conform to the MFMailComposeViewControllerDelegate
protocol, in addition to those already specified, so go ahead and add this to your header file.
The MFMailComposeViewControllerDelegate
protocol defines only one method, which you are required to implement in order to properly handle the completed use of the view controller by the user. You will give this a simple implementation to log the result.
-(void)mailComposeController:(MFMailComposeViewController *)controller
didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error
{
if (result == MFMailComposeResultSent)
NSLog(@"Mail Successfully Sent");
else if (result == MFMailComposeResultCancelled)
NSLog(@"Mail Cancelled");
else if (result == MFMailComposeResultFailed)
NSLog(@"Error, Mail Send Failed");
else if (result == MFMailComposeResultSaved)
NSLog(@"Mail Saved");
[self dismissModalViewControllerAnimated:YES];
}
Now, your new application will be able to present a view controller for sending mail, as shown in Figure 13–4. Unlike the MFMessageViewController
, however, you can actually test this functionality in the iOS simulator.
Quite conveniently, you can easily test all the functionalities of the MFMailComposeViewController
using the simulator without any fear of sending out multiple e-mails to any addresses, real or fake. The simulator will not actually send out your test messages over the Internet, so you can easily test your mailComposeDelegate
method's handling of the MailComposeResults
.
One additional issue that you may have to deal with when making use of the MFMailComposeViewController
occurs if you allow the making of the recipients to be based on userinput. It is highly possible that a user may incorrectly format a mail address. This will not cause your application to throw an exception, though it may cause your recipient to simply be ignored by the MFMailComposeViewController
. You can either format your user input to attempt to avoid this, or you can simply set up a regular expression to verify if the given address fits the required format.
The MFMailComposeViewController
includes functionality for you to attach data to your e-mail from your application through the use of the -addAttachmentData:mimeType:fileName:
method. This method takes three parameters:
attachment
:This instance of NSData
refers to the actual data of the object that you want to send. This means for any object you want to attach, you will need to acquire the NSData
for it.mimeType
: This property is an NSString
that defines to the controller the data type of the attachment. These values are not local to iOS, and so are not defined in the Apple documentation. They can, however, be easily found online. Wikipedia offers a very distinct article on all the possible values athttp://en.wikipedia.org/wiki/Internet_media_type
. The MIME type of a JPEG image, for example, is “image/jpeg”.fileName
: Use this NSString
property to set the preferred name for the file sent in the e-mail.You will add functionality to your application to access the user's image library, select an image, and then use that image to attach to your e-mail.
Start off by adding a UIImageView
underneath your UITextView
, along with a UIButton
underneath that with the label “Get Image”. Your view will now resemble that simulated in Figure 13–5.
You will need properties imageViewContent
andgetImageButton
, along with the method -getImagePressed:
for these. You will be presenting a UIPopoverController
from your getImageButton
, so make sure the sender
type of this method is set to a UIButton*
instead of id
. Go ahead and make a UIImage
property as well, called selectedImage
, to store a reference to the chosen image, making sure to properly synthesize it.
Whenever you want to access the photo library of an iPad, you need to use a UIImagePickerController
set inside of a UIPopoverController
. You will need to create a UIPopoverController
property called pop
, and make sure to synthesize it as well.
Before you continue, instruct your view controller to conform to the UIImagePickerControllerDelegate
and UIPopoverControllerDelegate
protocols. Overall your header file should now resemble the following:
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#import <MessageUI/MessageUI.h>
@interface MainViewController : UIViewController <UITextViewDelegate,
MFMessageComposeViewControllerDelegate, UINavigationControllerDelegate,
MFMailComposeViewControllerDelegate, UIImagePickerControllerDelegate,
UIPopoverControllerDelegate>
@property (strong, nonatomic) IBOutlet UITextView *textViewInput;
@property (strong, nonatomic) IBOutlet UIButton *textButton;
@property (strong, nonatomic) IBOutlet UIButton *mailButton;
@property (strong, nonatomic) IBOutlet UIImageView *imageViewContent;
@property (strong, nonatomic) IBOutlet UIButton *getImageButton;
@property (strong, nonatomic) UIImage *selectedImage;
@property (strong, nonatomic) UIPopoverController *pop;
-(IBAction)textPressed:(id)sender;
-(IBAction)mailPressed:(id)sender;
-(IBAction)getImagePressed:(UIButton *)sender;
///Pay close attention to the above (UIButton *) parameter type.
@end
If you have correctly handled all your new properties so far, your -viewDidUnload:
method should resemble the following:
- (void)viewDidUnload
{
[self setPop:nil];
[self setSelectedImage:nil];
[self setTextViewInput:nil];
[self setTextButton:nil];
[self setMailButton:nil];
[self setImageViewContent:nil];
[self setGetImageButton:nil];
[super viewDidUnload];
}
Now, you can write your -getImagePressed:
method to present your popover controller with access to the photo library.
-(void)getImagePressed:(UIButton *)sender
{
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
if ([UIImagePickerController
isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary])
{
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
picker.delegate = self;
self.pop = [[UIPopoverController alloc] initWithContentViewController:picker];
pop.delegate = self;
[pop presentPopoverFromRect:sender.frame inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
}
Now you just need to implement your UIImagePickerControllerDelegate
protocol methods.
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
[self.pop dismissPopoverAnimated:YES];
}
-(void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary *)info
{
UIImage *image = [info valueForKey:@"UIImagePickerControllerOriginalImage"];
self.selectedImage = image;
self.imageViewContent.image = image;
self.imageViewContent.contentMode = UIViewContentModeScaleAspectFill;
[self.pop dismissPopoverAnimated:YES];
}
At this point, your application can select an image and set it in your UIImageView
. If you are testing this application in the simulator, you will need to acquire at least one image to put in your simulator's photo library. You can do this by accessing the Safari app on the simulator, finding an image online, then clicking and holding the image to save it to the library. In Figure 13–6, your app is shown with an image already selected, with the Get Image button then pressed again.
Now you can continue to add the chosen image into your e-mail.You will modify your -mailPressed:
method to include attaching the image if one has been selected.
-(void)mailPressed:(id)sender
{
if ([MFMailComposeViewController canSendMail])
{
MFMailComposeViewController *mailVC = [[MFMailComposeViewController alloc] init];
[mailVC setSubject:@"SendItOut"];
[mailVC setToRecipients:[NSArray arrayWithObject:@"[email protected]"]];
[mailVC setMessageBody:self.textViewInput.text isHTML:NO];
mailVC.mailComposeDelegate = self;
/////NEW IMAGE CODE
if (self.selectedImage != nil)
{
NSData *imageData = UIImageJPEGRepresentation(self.selectedImage, 1.0);
[mailVC addAttachmentData:imageData mimeType:@"image/jpeg" fileName:@"SelectedImage"];
}
/////END OF NEW CODE
[self presentModalViewController:mailVC animated:YES];
}
else
{
NSLog(@"Error: Mail Unavailable");
}
}
Finally, you can modify your MFMailComposeViewController
's delegate method to properly reset your application.
-(void)messageComposeViewController:(MFMessageComposeViewController *)controller
didFinishWithResult:(MessageComposeResult)result
{
if (result == MessageComposeResultSent)
{
self.textViewInput.text = @"Message sent.";
self.selectedImage = nil;
self.imageViewContent.image = nil;
}
else if (result == MessageComposeResultFailed)
{
NSLog(@"Message Failed to Send!");
}
[self dismissModalViewControllerAnimated:YES];
}
If you test out the application in the simulator now and you attempt to send an e-mail after selecting an image, you should see the chosen image placed into your message, as in Figure 13–7.
Now that you have your application set up to be able to access both text and images, you can continue to enhance your functionality by adding the ability to print.
Before you specifically work on printing, you will reconfigure your application's user interface a bit to include your view controller inside of a UINavigationController
so that you can get a nice toolbar across the top. To do this, adjust your application delegate's -application:didFinishLaunchingWithOptions
method like so:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[MainViewController alloc] initWithNibName:@"MainViewController"
bundle:nil];
/////CHANGED CODE
__strong UINavigationController *navcon = [[UINavigationController alloc]
initWithRootViewController:self.viewController];
self.window.rootViewController = navcon;
/////END OF CHANGED CODE
[self.window makeKeyAndVisible];
return YES;
}
You may also have to move up the lower buttons in your view a bit in order to make sure they aren't pushed off-screen by the navigation bar.
Add the following lines to the end of your -viewDidLoad
method to configure your navigation bar.
self.title = @"Send It Out!";
if ([UIPrintInteractionController isPrintingAvailable])
{
UIBarButtonItem *printButton = [[UIBarButtonItem alloc]
initWithTitle:@"Print"style:UIBarButtonItemStyleBorderedtarget:self
action:@selector(printPressed:)];
self.navigationItem.rightBarButtonItem = printButton;
}
This condition will confirm to you that printing is possible on whichever device you run your application on before allowing the print button to be shown.
Now, you can continue on to implement your -printPressed:
method in order to add your printing functionality, primarily through the use of the UIPrintInteractionController
class. This class will be your “hub” of activity when it comes to configuring print jobs. You will go through and discuss the steps to setup this class individually before seeing the method as a whole.
Whenever you want to access an instance of a UIPrintInteractionController
, you simply call for a reference to the shared instance through the +sharedPrintController
class method.
UIPrintInteractionController *pic = [UIPrintInteractionController
sharedPrintController];
Up next, you must configure the printInfo
property of your controller, which specifies the settings for the print job.
UIPrintInfo *printInfo = [UIPrintInfo printInfo];
printInfo.outputType = UIPrintInfoOutputPhoto;
printInfo.jobName = self.title;
printInfo.duplex = UIPrintInfoDuplexLongEdge;
As you can see, you have set the outputType
to specify an image. The three possible values for this property are as follows:
UIPrintInfoOutputPhoto
: Used specifically for photos to be printedUIPrintInfoOutputGrayscale
: Used when dealing only with black text so as to improve performanceUIPrintInfoOutputGeneral
: Used for any mix of graphics and text, with or without colorYou did not yet set this printInfo
object as the printInfo
of your controller because you will do a little bit more configuration of it shortly.
Next, you have to do an interesting specification for your UIPrintInteractionController
. I say interesting because you absolutely have to do one, and only one, of four possible tasks:
UIPrintFormatter
to the controller to configure the layout of your page.UIPrintPageRenderer
, which can then have multiple instances of UIPrintFormatter
assigned to it to gain full customization of your content layout over multiple pages.You will start off with the simplest option of setting a single item to be printed. This item must be either an image or a PDF file to use these simpler options, so you will choose to simply print out your selectedImage
.
UIImage *image = self.selectedImage;
pic.printingItem = image;
Now that you know what you want to print, you can check the orientation of the image and configure your printInfo
accordingly.
if (!pic.printingItem&& image.size.width> image.size.height)
printInfo.orientation = UIPrintInfoOrientationLandscape;
pic.printInfo = printInfo;
pic.showsPageRange = YES;
Finally, you must simply present your UIPrintInteractionController
. This class is equipped with three different methods for presenting itself, depending on your specific implementation.
-presentFromBarButtonItem:animated:completionHandler:
: If you are writing for an iPad, this method is designed for use when theapplication's Print button is placed in a toolbar, such as yours.-presentFromRect:inView:animated:completionHandler:
: This method is also only for use with the iPad, but allows you to present the controller from any part of the view. Usually, the rect
specified will be the frame
of your Print button, wherever it is located.-presentAnimated:completionHandler:
: This method should be used whenever implementing printing on an iPhone due to the smaller screen.With this final method call, your -printPressed:
method in its entirety will look like so:
-(void)printPressed:(id)sender
{
if ([UIPrintInteractionController isPrintingAvailable] && (self.selectedImage != nil))
{
UIPrintInteractionController *pic = [UIPrintInteractionController
sharedPrintController];
UIPrintInfo *printInfo = [UIPrintInfo printInfo];
printInfo.outputType = UIPrintInfoOutputPhoto;
printInfo.jobName = self.title;
printInfo.duplex = UIPrintInfoDuplexLongEdge;
UIImage *image = self.selectedImage;
pic.printingItem = image;
if (!pic.printingItem&& image.size.width> image.size.height)
printInfo.orientation = UIPrintInfoOrientationLandscape;
pic.printInfo = printInfo;
pic.showsPageRange = YES;
[pic presentFromBarButtonItem:sender animated:YES
completionHandler:^(UIPrintInteractionController *printInteractionController, BOOL
completed, NSError *error)
{
if (!completed && (error != nil))
{
NSLog(@"Error due to Domain: %@, Code: %@", error.domain, error.code);
}
else
{
NSLog(@"Printing Cancelled");
}
}];
}
}
Now when you run your application, after selecting your image, a small controller will appear once you press the print button, from which you can select a printer and further configure your specific print job! Unfortunately, if you're testing this in your simulator or don't have any wireless printers set up, you won't see any available printers to use, as shown in Figure 13–8.
Luckily, when you installed the most recent version of Xcode, you were also given a fantastic application called Printer Simulator. With this program, you will be able to fully simulate print jobs from your application. It even gives you a PDF file of your simulated output, so you can see exactly how your image would have turned out without wasting any paper!
You can easily open this program by searching for it in Spotlight.
Upon running the Printer Simulator application, a variety of printer types will be automatically registered for use. It will look similar to Figure 13–9.
Now, upon testing your application, you should see multiple different types of simulated printers with which to test your application. You can choose different types to see how your printout is affected by the style of printer, as in Figure 13–10.
Once you have selected a printer, you have the option to print multiple copies as well as change the paper type before you print. At this point, you should start seeing activity in your Printer Simulator, and shortly afterward, a PDF file will be opened with your final printout, resembling that shown in Figure 13–11.
Expanding on your previous recipe, you will add functionality to make use of a print formatter to allow you to print simple text.
First, you will modify your -viewDidLoad
method to add an extra button to print the text in your UITextView
. Change the condition statement in the method that you've already made to look like so:
if ([UIPrintInteractionController isPrintingAvailable])
{
UIBarButtonItem *printButton = [[UIBarButtonItemalloc] initWithTitle:@"Print Image"
style:UIBarButtonItemStyleBordered target:self action:@selector(printPressed:)];
UIBarButtonItem *printTextButton = [[UIBarButtonItem alloc] initWithTitle:@"Print Text"
style:UIBarButtonItemStyleBordered target:self action:@selector(printTextPressed:)];
self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:printButton,
printTextButton, nil];
}
The new selector to print your text will then be implemented asfollows:
-(void)printTextPressed:(id)sender
{
if ([UIPrintInteractionController isPrintingAvailable])
{
UIPrintInteractionController *pic = [UIPrintInteractionController
sharedPrintController];
UIPrintInfo *printInfo = [UIPrintInfo printInfo];
printInfo.outputType = UIPrintInfoOutputGeneral;
printInfo.jobName = self.title;
printInfo.duplex = UIPrintInfoDuplexLongEdge;
pic.printInfo = printInfo;
UISimpleTextPrintFormatter *simpleTextPF = [[UISimpleTextPrintFormatter alloc]
initWithText:self.textViewInput.text];
simpleTextPF.startPage = 0;
simpleTextPF.contentInsets = UIEdgeInsetsMake(72.0, 72.0, 72.0, 72.0);
simpleTextPF.maximumContentWidth = 6*72.0;
pic.printFormatter = simpleTextPF;
pic.showsPageRange = YES;
[pic presentFromBarButtonItem:sender animated:YES
completionHandler:^(UIPrintInteractionController *printInteractionController, BOOL
completed, NSError *error)
{
if (!completed && (error != nil))
{
NSLog(@"Error due to Domain: %@, Code: %@", error.domain, error.code);
}
else
{
NSLog(@"Printing Cancelled");
}
}];
}
}
There are two main differences between this method and its predecessor:
outputType
property in your UIPrintInfo
is modified to the UIPrintInfoOutputGeneral
value, since you are no longer printing photos.UIImage
to the printingItem
property, you set an instance of UISimpleTextPrintFormatter
to the printFormatter
property. This object is initialized with the desired text, and then configured through its properties.
statePage
property will be used more at a later point, but allows you to specify the page in your job for your formatter to be applied to.When printing simple text, it is also quite easy to apply the preceding method to printing out HTML-formatted text. To do this, simply make use of a UIMarkupTextPrintFormatter
instead of a UISimpleTextPrintFormatter
.
Just as before, by using the Printer Simulator, you can generate your test output. Since you set your text view's text as the content of your print formatter, you will simply get a document with some Lorem Ipsum text in it, as in Figure 13–12.
Just as you can print text using a UISimpleTextPrintFormatter
, you are easily able to print the contents of a view using another subclass of UIPrintFormatter
: UIViewPrintFormatter
.
Start by modifying your -viewDidLoad
's conditional setup to now appear like so:
if ([UIPrintInteractionController isPrintingAvailable])
{
UIBarButtonItem *printButton = [[UIBarButtonItem alloc] initWithTitle:@"Print Image"
style:UIBarButtonItemStyleBorderedtarget:self action:@selector(printPressed:)];
UIBarButtonItem *printTextButton = [[UIBarButtonItem alloc] initWithTitle:@"Print Text"
style:UIBarButtonItemStyleBordered target:self action:@selector(printTextPressed:)];
UIBarButtonItem *printViewButton = [[UIBarButtonItem alloc] initWithTitle:@"Print View"
style:UIBarButtonItemStyleBordered target:self action:@selector(printViewPressed:)];
self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:printButton,
printTextButton, printViewButton, nil];
}
Your newest printing method,-printViewPressed:
, will closely resemble your previous one, with the key change of using a UIViewPrintFormatter
.
-(void)printViewPressed:(id)sender
{
if ([UIPrintInteractionController isPrintingAvailable])
{
UIPrintInteractionController *pic = [UIPrintInteractionController
sharedPrintController];
UIPrintInfo *printInfo = [UIPrintInfo printInfo];
printInfo.outputType = UIPrintInfoOutputGeneral;
printInfo.jobName = self.title;
printInfo.duplex = UIPrintInfoDuplexLongEdge;
printInfo.orientation = UIPrintInfoOrientationLandscape;
pic.printInfo = printInfo;
UIViewPrintFormatter *viewPF = [self.textViewInput viewPrintFormatter];
pic.printFormatter = viewPF;
pic.showsPageRange = YES;
[pic presentFromBarButtonItem:sender animated:YES
completionHandler:^(UIPrintInteractionController *printInteractionController, BOOL
completed, NSError *error)
{
if (!completed && (error != nil))
{
NSLog(@"Error due to Domain: %@, Code: %@", error.domain, error.code);
}
else
{
NSLog(@"Printing Cancelled");
}
}];
}
}
Unfortunately, the UIViewPrintFormatter
is, at the moment, currently configured only to provide printing views of three system views: UITextView
, UIWebView
, and MKMapView
(from the MapKit framework as discussed in Chapter 4). Since your application makes use of only one of these, you will simply have it print your UITextView
's view, resulting in an output like that in Figure 13–13.
Despite the UIViewPrintFormatter
's limitations, it can be an incredibly easy to way easily print out the contents of any text, map, or web page.
A page renderer is essentially what allows you to fully customize the content of any print job. It allows you to not only format multiple pages with different print formatters, but also draw custom content in the header, body, and footer of any page.
In order to use a page renderer, you must create a custom subclass of the UIPrintPageRenderer
class, from which you can override methods to customize the content of your printing job.
Create a new file, using the Objective-C class template. When you enter your file name of “SendItOutPageRenderer”, make sure that the file will be a subclass of UIPrintPageRenderer
, as in Figure 13–14.
Click through to create your new file.
Next, define two NSString
properties,title
and author
, to be printed in the header of your renderer.
#import <UIKit/UIKit.h>
@interface SendItOutPageRenderer : UIPrintPageRenderer
@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSString *author;
@end
In order to customize the layout of your specific page renderer, you can override methods inherited from the UIPrintPageRenderer
class. The way that this class is set up is that the -drawPageAtIndex:inRect:
method then calls four other methods:
-drawHeaderForPageAtIndex:inRect:
: Used to specify header content; if the headerHeight
property of the renderer is zero, this method will not be called.-drawContentForPageAtIndex:inRect:
: Draws custom content within the page's content rectangle-drawFooterForPageAtIndex:inRect:
: Specifies footer content; this method will also not be called if the renderer's footerHeight
property is zero.-drawPrintFormatter:forPageAtIndex:
:Uses a combination of print formatters and custom content to overlay or fill in a viewYou are able to override any of thesefive methods (including -drawPageAtIndex:inRect:
) in order to customize your printing content. In your case, you will override the header, footer, and print-formatter methods.
You will have your header print out the document's author on the left, and the title on the right. Your method will then look like so:
- (void)drawHeaderForPageAtIndex:(NSInteger)pageIndex inRect:(CGRect)headerRect
{
if (pageIndex != 0)
{
UIFont *font = [UIFont fontWithName:@"Helvetica"size:12.0];
CGSize titleSize = [self.title sizeWithFont:font];
CGFloat drawXTitle = CGRectGetMaxX(headerRect) - titleSize.width;
CGFloat drawXAuthor = CGRectGetMinX(headerRect);
CGFloat drawY = CGRectGetMinY(headerRect);
CGPoint drawPointAuthor = CGPointMake(drawXAuthor, drawY);
CGPoint drawPointTitle = CGPointMake(drawXTitle, drawY);
[self.title drawAtPoint:drawPointTitle withFont:font];
[self.author drawAtPoint:drawPointAuthor withFont:font];
}
}
Your footer-implementation method will look similar, and will print out a centered page number. Since the page indexes start with 0, you must remember to increment all your values by 1.
- (void)drawFooterForPageAtIndex:(NSInteger)pageIndex inRect:(CGRect)footerRect
{
UIFont *font = [UIFont fontWithName:@"Helvetica"size:12.0];
NSString *pageNumber = [NSString stringWithFormat:@"%d.", pageIndex+1];
CGSize pageNumSize = [pageNumber sizeWithFont:font];
CGFloat drawX = CGRectGetMaxX(footerRect)/2.0 - pageNumSize.width - 1.0;
CGFloat drawY = CGRectGetMaxY(footerRect) - pageNumSize.height;
CGPoint drawPoint = CGPointMake(drawX, drawY);
[pageNumber drawAtPoint:drawPoint withFont:font];
}
Finally, to deal with interlaced print formatters, you will implement the -drawPrintFormatter:forPageAtIndex:
method to overlay a simple text over your view. This could easily be used to place some kind of “Proprietary Content” label over images or documents in a more targeted application.
-(void)drawPrintFormatter:(UIPrintFormatter *)printFormatter
forPageAtIndex:(NSInteger)pageIndex
{
CGRect contentRect = CGRectMake(self.printableRect.origin.x,
self.printableRect.origin.y+self.headerHeight, self.printableRect.size.width,
self.printableRect.size.height-self.headerHeight-self.footerHeight);
[printFormatter drawInRect:contentRect forPageAtIndex:pageIndex];
NSString *overlayText = @"Overlay Text";
UIFont *font = [UIFont fontWithName:@"Helvetica"size:26.0];
CGSize overlaySize = [overlayText sizeWithFont:font];
CGFloat xCenter = CGRectGetMaxX(self.printableRect)/2.0 - overlaySize.width/2.0;
CGFloat yCenter = CGRectGetMaxY(self.printableRect)/2.0 - overlaySize.height/2.0;
CGPoint overlayPoint = CGPointMake(xCenter, yCenter);
[overlayText drawAtPoint:overlayPoint withFont:font];
}
In this method, it is important to note that you must draw the content of each printFormatter
manually using its own -drawInRect:forPageAtIndex:
method. In order to avoid covering your header or footer, you specified a drawing area restricted by the headerHeight
and footerHeight
.
Now, back in your main view controller, make sure to import the newly created SendItOutPageRenderer.h
file.
#import "SendItOutPageRenderer.h"
Add a final extra UIBarButtonItem
to present a Print Custom option to your user. Including all functions from your previous recipes, your -viewDidLoad
method should now read like so:
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(availabilityChange:)
name:@"MFMessageComposeViewControllerTextMessageAvailabilityDidChangeNotification"
object:nil];
self.textViewInput.layer.cornerRadius = 15.0;
self.textViewInput.delegate = self;
self.title = @"Send It Out!";
if ([UIPrintInteractionController isPrintingAvailable])
{
UIBarButtonItem *printButton = [[UIBarButtonItem alloc] initWithTitle:@"Print Image"
style:UIBarButtonItemStyleBordered target:self action:@selector(printPressed:)];
UIBarButtonItem *printTextButton = [[UIBarButtonItem alloc] initWithTitle:@"Print Text"
style:UIBarButtonItemStyleBordered target:self action:@selector(printTextPressed:)];
UIBarButtonItem *printViewButton = [[UIBarButtonItem alloc] initWithTitle:@"Print View"
style:UIBarButtonItemStyleBordered target:self action:@selector(printViewPressed:)];
UIBarButtonItem *printCustomButton = [[UIBarButtonItem alloc] initWithTitle:@"Print
Custom" style:UIBarButtonItemStyleBordered target:self
action:@selector(printCustomPressed:)];
self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:printButton,
printTextButton, printViewButton, printCustomButton, nil];
}
}
Finally, you can implement your -printCustomPressed:
action.
-(void)printCustomPressed:(id)sender
{
if ([UIPrintInteractionControllerisPrintingAvailable])
{
UIPrintInteractionController *pic = [UIPrintInteractionController
sharedPrintController];
UIPrintInfo *printInfo = [UIPrintInfoprintInfo];
printInfo.outputType = UIPrintInfoOutputGeneral;
printInfo.jobName = self.title;
printInfo.duplex = UIPrintInfoDuplexLongEdge;
printInfo.orientation = UIPrintInfoOrientationPortrait;
pic.printInfo = printInfo;
UISimpleTextPrintFormatter *simplePF = [[UISimpleTextPrintFormatter alloc]
initWithText:[self.textViewInput.text stringByAppendingString:@"THIS TEXT IS MY FIRST
PAGE"]];
UIViewPrintFormatter *viewPF = [self.textViewInputview PrintFormatter];
SendItOutPageRenderer *sendPR = [[SendItOutPageRendereralloc] init];
sendPR.title = @"My Print Job Title";
sendPR.author = @"Document Author";
sendPR.headerHeight = 72.0/2;
sendPR.footerHeight = 72.0/2;
[sendPR addPrintFormatter:simplePF startingAtPageAtIndex:0];
[sendPR addPrintFormatter:viewPF startingAtPageAtIndex:1];
pic.printPageRenderer = sendPR;
pic.showsPageRange = YES;
[pic presentFromBarButtonItem:sender animated:YES
completionHandler:^(UIPrintInteractionController *printInteractionController, BOOL
completed, NSError *error)
{
if (!completed && (error != nil))
{
NSLog(@"Error due to Domain: %@, Code: %@", error.domain, error.code);
}
else
{
NSLog(@"Printing Cancelled");
}
}];
}
}
This method includes the following extra steps from your previous recipe:
UIWebView
or MKMapView
in this application, you have simply chosen to print your UITextView
's text, as well as its overall view.SendItOutPageRenderer
class, and configure it with a title
, author
, headerHeight
, and footerHeight
. If you did not specify the last two of these, your header and footer customization methods would not be called.UIPrintInteractionController
.Upon testing this new functionality, your output will be a two-page text document, complete with simple headers, footers, and even a text overlay, as shown in Figure 13–15.
Due to the simplicity of your application, the screenshot in Figure 13–15may not look like much, but considering your application of custom headers, footers, overlay content, and page formatters, it actually gives a very good representation of the power of making use of a page renderer for printing when striving for ideal customizations.
You are responsible, when creating your applications, to always have the user in mind. Every single aspect of your application should be designed to both allow and helpthe user to accomplish a goal, and each aspect should then be optimized to expedite these goals. Functionalities to transmit data, such as sending text messages, constructing e-mails, or creating printouts, tend to be overlooked as unnecessary in this process, and most often erroneously. Developers must always be careful to think from the user's standpoint, and imagine what a user could do with any given feature. The simple possibility of printing content for later use, or being able to easily e-mail an interesting image to a friend, could easily be the dividing line between what makes a customer buy your app over someone else's. By understanding and utilizing these “extra” functionalities, you are able to drastically improve the functionality of your applications in order to better serve your endusers.