Have you ever been asked to create PDF documents with content straight from your app? Have you ever even thought how to do that, if you’ve never done it before?
Well, beginning the post by setting questions is a bit of unorthodox way to start, but the above summarise what I’m about to talk in this text. The idea of creating a PDF document in an iOS app looks quite often like the road through hell, but it doesn’t have to be like that. As a developer, you have always to be resourceful and create yourself alternatives and options to choose from and avoid reaching your goal at any cost. Nobody wants that. I have to admit that drawing a PDF page manually can become eventually a really painful process (depending on the content), and doing so can turn into an unproductive task at the end. Calculating points, adding lines, setting colors, insets, offsets, etc, might be interesting (or not), but definitely things can go really messy if you have complicated documents to draw.
My goal here is to present you a different way to create PDF documents, which is way more simpler than drawing them manually. The whole idea is based on using HTML templates and it can be synopsised in the following steps:
- Creation of HTML templates for the forms or the content needed to be printed to PDF.
- Use of those HTML templates to produce the real content (optionally display it in a web view).
- Printing the real HTML content to PDF.
In the last step, iOS will do all the hard work for you.
Let me be more thorough now. I think that you totally agree to the fact that it’s always preferable to deal with HTML instead of drawing PDFs directly. All you actually want in that case is to manage to represent your content as an HTML page, but it’s not wise or productive to manually create pages for repeated content. For example, think of an app that prints or exports personal information for school students to PDF. Creating an HTML page manually for every single student is out of the question, as the same pattern is followed for printing the info for all students. What you really want is to create one HTML page only, which is going to be the template. Instead of using real values, you set placeholders in the key points that you mark in a unique way, and then inside your app you replace those placeholders with real values. You understand of course that the value replacement in the last step is an automated process and can be done repeatedly.
By the time you have some real content formed as HTML code, you can do whatever it’s possible to do with it. That means that you can display it in a web view, to save it to external files, to share it, and of course, to print it as PDF.
So, what exactly are we going to go through the next parts?
The ultimate goal is to show to you how to print content to a PDF document. But we’ll get there based on HTML templates with placeholder values that will be replaced by real values. The demonstration app we’ll use is a simple invoice maker, which I think that perfectly fits to the needs of printing data to PDF. We are not going to build that app from scratch, it’s not our goal after all. The default functionality will be given to you already made. You’ll also be given the HTML templates, and we’ll go through them so you have the chance to understand what is all about, and what the meaning of placeholders is. But no matter what, we’ll go together and step by step through the process of composing the real HTML content, and then printing it to a PDF document. And we won’t stop there; I’ll also show you how to add header and footer content to your final PDF.
Interested in all the above? Let’s get started then!
The Starter Project
We’ll begin by taking a quick tour to the demo app of this tutorial, which actually is an invoice maker tool. Before I start talking about it, you have to download a starter project first that we’ll work on. Once you do so, open it in Xcode.
In that starter project you’ll find quite enough job already been done. The “landing” view controller named InvoiceListViewController
is used to display a list of invoices created and saved inside the app. This is also the place where you can initiate the creation of a new invoice by using the plus bar button to the top-right side of the screen. By tapping on any row matching to an invoice you’ll be guided to a preview screen, where you can see and print the invoice as a PDF document. Note that this part of functionality doesn’t exist in the starter project; it’s what we are going to do in this tutorial. Finally, a created invoice can be deleted by swiping to the left on a cell. The following screenshot demonstrates that view controller:
As I said, a new invoice can be created by tapping on the plus button. That action takes us to a new view controller called CreatorViewController
which looks like that:
An invoice has generally a variety of values needed to be filled in before it can be printed. Some of those values can be set manually in that view controller, some others are calculated automatically, and some others will be hardcoded later in code. To make that specific, the values that can be manually added in our demo app are:
- The recipient info, which actually is the person the invoice is addressed to. This is the grey area in the above figure.
- The invoice items, where each item has two parts: The description of the service provided, and the price for that service. We’ll keep things simple and we’ll have no VAT here. An item can be added here by using the plus button in the toolbar to the bottom of the screen (more about it in a while).
The automatically calculated values are:
- The invoice number (the number shown as a title to the navigation bar).
- The total amount of the invoice (shown to the left side of the bottom toolbar).
Lastly, the invoice values that we’ll hardcode later are:
- The sender information, which is the issuer’s information.
- The due date of the invoice (that we won’t use but set it to blank in case you want to use it).
- The payment method.
- The logo of the invoice.
Regarding the invoice items, a new view controller named AddItemViewController
exists in the project for allowing easy data entry. There are two textfields only for filling the respective values in, and a save button to add them to the invoice and return to the previous view controller:
All the invoice items are appended to an array of dictionaries, where each dictionary has two values: The description and the price of each item. That array is used as the datasource for the tableview that lists all items in the CreatorViewController
. When an invoice gets saved, then both the manually added data and the automatically calculated data is set to a dictionary and being returned to the InvoiceListViewController
. Here’s what is returned:
- The invoice number (string value)
- The recipient info (string value)
- The total amount (string value)
- The invoice items (array with dictionaries).
Upon saving an invoice the next invoice number is calculated and stored for future use in the user defaults dictionary (NSUserDefaults
). The dictionary with the data of a new invoice is appended to an array in the InvoiceListViewController
, and that array is stored to the user defaults as well every time a new one is created by the user. The invoice data is loaded from the user defaults when that view controller is about to appear. Keep in mind that saving to user defaults dictionary the main data of the app is a quick solution for the sake of the demo app, and it’s not recommended as an approach in a real application. There are definitely better ways to store your data.
I have almost nothing to comment regarding the existing code, and all you have to do is to go through each view controller or follow the flow of the app so as you see the details of the implementation. There’s just one point I’d like to mention about, and that is the AppDelegate.swift
file. You’ll find three convenient methods there: One for getting easy access to the application delegate, one for getting the path to the documents directory, and one for converting an amount represented as a string into a currency string (the amount along with the proper currency symbol). Even though these methods are already being used in the starter project, we’re going to make use of them again later as well. In the AppDelegate
you’ll also find a property named currencyCode
set to “eur” (Euro currency) by default. Use it to set your own currency code.
Lastly, let me tell you where the starter project ends, and where exactly we’re about to start here. By tapping on an existing invoice in the tableview in the InvoiceListViewController
, a dictionary containing the data of the matching invoice is passed to the PreviewViewController
view controller. In this one, there’s a web view to preview the invoice rendered as an HTML document, and a button to export to PDF. These functionalities do not exist in the starter project and we will start implementing them, taking as granted that all the data we need regarding an invoice already exists in the PreviewViewController
and we can use it directly.
The HTML Template Files
As I explained in the introduction, we’re going to use HTML templates to initially produce an invoice, and then the real HTML content will be printed to a PDF file. The primary logic here is to put placeholders in the HTML files in certain points, and then replace those placeholders with real data. But in order to do that, we must find or create custom HTML forms that will give us eventually the desired result. For the purposes of this tutorial we won’t create any custom HTML invoice templates. Instead, we’ll use the one found here (with special thanks to the creators). That template has been modified a little bit, so there’s no more shadow and a border around it, while a grey background color has been applied to the logo.
In the starter project that you’ve downloaded, you’ll find three HTML files:
- invoice.html
- last_item.html
- single_item.html
The first one contains the code that will produce the entire invoice, except for the lines of the items. We have two other templates for the items specifically: single_item.html
will be used to display an item in any row except for the last one, and last_item.html
will be used to represent the last item only. That’s because the bottom border line is different for the last row.
A placeholder inside any HTML template file will be a special keyword surrounded by # symbols. For example, the next extract show the placeholders for the invoice number, issue date and due date:
Invoice #: #INVOICE_NUMBER
#INVOICE_DATE#
#DUE_DATE#
You can find all placeholders inside the three HTML files, and the position where each placeholder fits to. Here’s a list with all of them:
- #LOGO_IMAGE#
- #INVOICE_NUMBER#
- #INVOICE_DATE#
- #DUE_DATE#
- #SENDER_INFO#
- #RECIPIENT_INFO#
- #PAYMENT_METHOD#
- #ITEMS#
- #TOTAL_AMOUNT#
- #ITEM_DESC#
- #PRICE#
The last two placeholders exist in the single_item.html and last_item.html files only. Also, the #ITEMS# placeholder will be replaced once the respective code for all items has been created by using the two extra HTML template files (details about that in the proper time).
As you can see, preparing one or more HTML templates in order to create custom output for a form (in that case it’s the invoice) it’s not difficult. And after having gone through the whole process here, you’ll realise that generating real content based on those templates and then exporting it to PDF files is easy and effective.
Composing the Content
Having gone through the demo app and the invoice templates, it’s time for us now to get started and complete the app by implementing all the key missing parts. What we want to do initially, is to use the HTML templates and create the actual HTML content for an invoice that gets selected in the first view controller (InvoiceListViewController
). After having done that, we’ll display the generated HTML code in the web view existing in the PreviewViewController
view controller, and we’ll be able to verify that way that the job has been properly done.
The main and most important task in this part is the replacement of the placeholder values in the invoice HTML template files with real values. Those real values are actually the data matching to the selected invoice in the InvoiceListViewController
that pass to the PreviewViewController
. As you’ll see next, the replacement of the placeholders is a straightforward task. Before we get to that though, let’s create a new class that we’ll use for generating the real the HTML content and later to print to PDF. So, in Xcode go to File > New > File… menu and create a new Cocoa Touch Class. Make it a subclass of the NSObject
class, and name it InvoiceComposer. Proceed with the guide and get finished with the new file creation.
With the InvoiceComposer.swift
file now existing in Xcode, select it in the Project Navigator to open it. We’ll begin by declaring some properties (both constants and variables):
class InvoiceComposer: NSObject { let pathToInvoiceHTMLTemplate = NSBundle.mainBundle().pathForResource("invoice", ofType: "html") let pathToSingleItemHTMLTemplate = NSBundle.mainBundle().pathForResource("single_item", ofType: "html") let pathToLastItemHTMLTemplate = NSBundle.mainBundle().pathForResource("last_item", ofType: "html") let senderInfo = "Gabriel Theodoropoulos
123 Somewhere Str.
10000 - MyCity
MyCountry" let dueDate = "" let paymentMethod = "Wire Transfer" let logoImageURL = "http://www.appcoda.com/wp-content/uploads/2015/12/blog-logo-dark-400.png" var invoiceNumber: String! var pdfFilename: String! }
With the first three properties (pathToInvoiceHTMLTemplate
, pathToSingleItemHTMLTemplate
, pathToLastItemHTMLTemplate
) we specify the paths to the HTML template files. These paths will become handy in a while as we’ll need to open them all and get the template code in order to modify it.
As I have already said, our demo app doesn’t provide options to set all invoice parameters (senderInfo
, dueDate
, paymentMethod
, logoImageURL
), so the specific ones are hardcoded here. In a real app of course, these would consist of values that users should be able to set or change. The last one is the URL of the image that I’ve chosen as the logo for the invoices. Needless to say that you are totally free to set your own values in the above properties (for example, set your own info in the senderInfo
property).
Lastly, the invoiceNumber
property will hold the number of the invoice that’s being previewed at any given moment, and the pdfFilename
will contain the path to the PDF file. That’s something we’ll need later, not now; however it’s the best time to declare it so we’re ready to use it when the time comes.
Except for the above properties, add the default init()
method to the class:
class InvoiceComposer: NSObject { ... override init() { super.init() } }
We can now proceed and create a new method that will do the important work of replacing the placeholder values in the HTML template files. We’ll name it renderInvoice
, and here are the parameters that will get:
func renderInvoice(invoiceNumber: String, invoiceDate: String, recipientInfo: String, items: [[String: String]], totalAmount: String) -> String! { }
The parameters are actually all the values that can be manually set when creating an invoice in the demo app, and they are all we need in order to generate the invoice (along with the hardcoded values as properties of this class). What we’ll get back from it is a String value that will contain the final HTML with the real content.
Let’s start implementing that method and let’s perform our first important tasks. In the following snippet, two significant things are taking place: At first the template content from the invoice.html
file is loaded to a string variable so we can modify it, and then we replace all the placeholders at once except for the invoice items. There are comments that will help you go through it:
func renderInvoice(invoiceNumber: String, invoiceDate: String, recipientInfo: String, items: [[String: String]], totalAmount: String) -> String! { // Store the invoice number for future use. self.invoiceNumber = invoiceNumber do { // Load the invoice HTML template code into a String variable. var HTMLContent = try String(contentsOfFile: pathToInvoiceHTMLTemplate!) // Replace all the placeholders with real values except for the items. // The logo image. HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#LOGO_IMAGE#", withString: logoImageURL) // Invoice number. HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#INVOICE_NUMBER#", withString: invoiceNumber) // Invoice date. HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#INVOICE_DATE#", withString: invoiceDate) // Due date (we leave it blank by default). HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#DUE_DATE#", withString: dueDate) // Sender info. HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#SENDER_INFO#", withString: senderInfo) // Recipient info. HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#RECIPIENT_INFO#", withString: recipientInfo.stringByReplacingOccurrencesOfString("\n", withString: "
")) // Payment method. HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#PAYMENT_METHOD#", withString: paymentMethod) // Total amount. HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#TOTAL_AMOUNT#", withString: totalAmount) } catch { print("Unable to open and use HTML template files.") } return nil }
Replacing a placeholder value is as simple as it looks in the code above. By using the stringByReplacingOccurrencesOfString(...)
string method, we provide the placeholder as the first argument (string to be replaced) and the real value as the second (string to replace), and that’s all for it. It might be a little “boring” to do the same thing for a big number of placeholders, but it’s not difficult in any case at all.
Further than that, notice that all the work is taking place inside a do-catch
statement, as the initialisation of the HTMLContent
string with the contents of a file can throw an exception. Also, notice that if something goes wrong we return nil, and for now there’s no real value returned with the actual HTML content; it’s coming next.
Let’s focus now on setting the invoice items. As their number may vary, we’ll use a loop to handle them. For each item other than the last one, we’ll be opening the single_item.html
template file and we’ll be replacing its placeholders. For the last item only though, we’ll use the last_item.html
template as the bottom border line is different. The resulting HTML code will be appended to another string value (the allItems
variable you’ll see next), and once that string has been composed with the details of all items, it’ll be used to replace the #ITEMS# placeholder in the HTMLContent
string. At the end that string is returned from the method.
Append the following code snippet inside the do
body:
func renderInvoice(invoiceNumber: String, invoiceDate: String, recipientInfo: String, items: [[String: String]], totalAmount: String) -> String! { ... do { ... // The invoice items will be added by using a loop. var allItems = "" // For all the items except for the last one we'll use the "single_item.html" template. // For the last one we'll use the "last_item.html" template. for i in 0..<items.count { var itemHTMLContent: String! // Determine the proper template file. if i != items.count - 1 { itemHTMLContent = try String(contentsOfFile: pathToSingleItemHTMLTemplate!) } else { itemHTMLContent = try String(contentsOfFile: pathToLastItemHTMLTemplate!) } // Replace the description and price placeholders with the actual values. itemHTMLContent = itemHTMLContent.stringByReplacingOccurrencesOfString("#ITEM_DESC#", withString: items[i]["item"]!) // Format each item's price as a currency value. let formattedPrice = AppDelegate.getAppDelegate().getStringValueFormattedAsCurrency(items[i]["price"]!) itemHTMLContent = itemHTMLContent.stringByReplacingOccurrencesOfString("#PRICE#", withString: formattedPrice) // Add the item's HTML code to the general items string. allItems += itemHTMLContent } // Set the items. HTMLContent = HTMLContent.stringByReplacingOccurrencesOfString("#ITEMS#", withString: allItems) // The HTML code is ready. return HTMLContent } catch { print("Unable to open and use HTML template files.") } return nil }
MARKDOWN_HASH9be45a8449d308e6aba6ff17060c2c17MARKDOWN_HASH
and MARKDOWN_HASH43ec7db1bdfab19b8c7d9b31b17d0ccdMARKDOWN_HASH
methods in the AppDelegate.swift file.That’s all for now. The template code has been modified appropriately in order to produce an invoice with real content. Next, we’ll make use of the results of the above method.
Previewing the HTML Content
After having created the real content for the invoice, it’s time to verify that the work has been properly done. Therefore, our goal in this part is to load the composed HTML string to the web view existing in the PreviewViewController, and watch the results of our previous efforts. Note that this is an optional step and in real apps it isn’t necessary to use a web view for previewing the HTML before printing to PDF. We mostly do it here for the sake of the completeness of the demo app.
Switch now to the PreviewViewController.swift file, and go to the top of the class. For starters we’ll declare a couple of new properties:
class PreviewViewController: UIViewController { ... var invoiceComposer: InvoiceComposer! var HTMLContent: String! }
The first one is an object of the class we created in the previous part and that we’ll initialise shortly. The HTMLContent
string variable will be used to hold the real HTML content for future use.
Next, we’ll create a new method where we’ll do the following:
- We’ll initialise the
invoiceComposer
object. - We’ll call the
renderInvoice(...)
method to generate the invoice HTML code. - We’ll load that HTML to the web view.
- We’ll assign the returned HTML string to the
HTMLContent
property.
Let’s see that:
func createInvoiceAsHTML() { invoiceComposer = InvoiceComposer() if let invoiceHTML = invoiceComposer.renderInvoice(invoiceInfo["invoiceNumber"] as! String, invoiceDate: invoiceInfo["invoiceDate"] as! String, recipientInfo: invoiceInfo["recipientInfo"] as! String, items: invoiceInfo["items"] as! [[String: String]], totalAmount: invoiceInfo["totalAmount"] as! String) { webPreview.loadHTMLString(invoiceHTML, baseURL: NSURL(string: invoiceComposer.pathToInvoiceHTMLTemplate!)!) HTMLContent = invoiceHTML } }
There’s nothing particularly difficult above, just pay attention to the arguments passing to the renderInvoice(...)
method. Once we get an actual HTML string back from that method (not a nil value), we load it to the web view.
Time to call our new method as shown next:
override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) createInvoiceAsHTML() }
If you want to see the results, run the app and create a new invoice (if you haven’t done it yet). Then select it from the list by tapping on it, and watch the invoice content appearing on the web view, like the one shown below:
Preparing to Print
With the half of the job already done, we can start working in the printing process which will become our way to export an invoice to PDF. There is a special class that we are going to use for our purposes, named UIPrintPageRenderer
. If you’ve never used it or heard of it, then let me tell you in short that this class is the one capable of rendering content for printing (either to a file or to a printer using AirPrint). Here‘s the official documentation page, please take a look for more information about that class.
The UIPrintPageRenderer
class provides various drawing methods that we don’t really need to override in simple cases. Those drawing methods can be overridden by subclassing the UIPrintPageRenderer
class only, but it worths the extra effort as we gain great flexibility and control over the content that will be printed to the actual page, the header or the footer. And in this demo app we’ll need to subclass it because we are going to draw custom header and footer (in a while).
So, back to Xcode again, create a new class following same steps as before. Two things to take care about:
- Make it a subclass of the
UIPrintPageRenderer
class. - Name it
CustomPrintPageRenderer
.
Once you have it ready (at that point you should be able to see a new file in the Project Navigator called CustomPrintPageRenderer.swift
), go there for doing some quick preparation for the next steps. Initially, let’s specify the width and height (in pixels) of the A4 page. Remember that we want the invoice to be exported to PDF, but that PDF could be printed to a real printer as well, so it’s important to limit the paper frame.
class CustomPrintPageRenderer: UIPrintPageRenderer { let A4PageWidth: CGFloat = 595.2 let A4PageHeight: CGFloat = 841.8 }
The above values describe the precise width and height of the normal, common A4 page that is used all over the world for printing.
It’s necessary to specify now the paper frame and the printing area that an object of our CustomPrintPageRenderer
class will draw inside. We’ll do that in the init()
method, where obviously the above two properties are meant to be used:
override init() { super.init() // Specify the frame of the A4 page. let pageFrame = CGRect(x: 0.0, y: 0.0, width: A4PageWidth, height: A4PageHeight) // Set the page frame. self.setValue(NSValue(CGRect: pageFrame), forKey: "paperRect") // Set the horizontal and vertical insets (that's optional). self.setValue(NSValue(CGRect: pageFrame), forKey: "printableRect") }
The above consists of a quite straightforward, and at the same time a standard technique for setting the frame of the paper and the area where content will be printed. Both the paperRect
and printableRect
properties are read-only, and that’s why we set their values that way.
In that snippet, you can see that we made the paper frame and the printable area equal. However, you’ll be wanting to set some insets to the printable area (offset from the page edges), so you end up with better printing results. In that case, you could just replace the last line with the next one:
self.setValue(NSValue(CGRect: CGRectInset(pageFrame, 10.0, 10.0)), forKey: "printableRect")
The above adds an offset of 10 points to both horizontal and vertical axis. Note that the configuration made in this part should be done even if you don’t subclass the UIPrintPageRenderer
. In other words, you should never forget to set the paper and printable area of your printing object.
Printing To PDF
By saying “printing to PDF” we actually mean drawing some content to a PDF graphics context. Once that happens, the drawn content can be sent either to a real printer or stored to a file. As we are interested in the second case only, we’ll draw the HTML content into a PDF context, we’ll get the results of the drawing back as a NSData
object, and then we’ll save that object to a file (the final .pdf file). Various, but simple steps are part of the process, so let’s see everything one by one.
We’ll proceed here by opening the InvoiceComposer.swift
file, where we’ll implement a new method named exportHTMLContentToPDF(...)
. It will accept one parameter only; the HTML content we want to export to PDF. Before we see the implementation however, it’s necessary to present another printing-related concept. That is the print formatter (UIPrintFormatter
class). The following extract is taken from Apple’s documentation:
UIPrintFormatter is an abstract base class for print formatters: objects that lay out custom printable content that can cross page boundaries. Given a print formatter, the printing system can automate the printing of the type of content associated with the print formatter.
That means for us that simply by adding the HTML content as a print formatter to the print page renderer, the iOS printing system will take over of the layout and actual printing to the page. I would advice you to take a look at this page for more information, as everything is explained there. But to keep it simple, think of the print formatter as the mean that we’ll use to pass the content we want to print to the printing system of iOS. In addition to that, it’s necessary to say that even though the UIPrintFormatter
is an abstract class, iOS SDK provides concrete subclasses of it to make use of. One of them is the UIMarkupTextPrintFormatter
, and that’s what we need for adding the HTML content (markup content) to the print page renderer object. More details about it and for the rest subclasses can be found in the link above.
By having said that, it’s about time to implement our new method. Here it is:
func exportHTMLContentToPDF(HTMLContent: String) { let printPageRenderer = CustomPrintPageRenderer() let printFormatter = UIMarkupTextPrintFormatter(markupText: HTMLContent) printPageRenderer.addPrintFormatter(printFormatter, startingAtPageAtIndex: 0) let pdfData = drawPDFUsingPrintPageRenderer(printPageRenderer) pdfFilename = "\(AppDelegate.getAppDelegate().getDocDir())/Invoice\(invoiceNumber).pdf" pdfData.writeToFile(pdfFilename, atomically: true) print(pdfFilename) }
Here’s what happens in that code snippet:
- Initially we initialise a
CustomPrintPageRenderer
object that we’ll use to perform the actual drawing (what we call printing). - Next we instantiate a
UIMarkupTextPrintFormatter
object, where we pass the HTML content as a parameter to the initialisation. - The page formatter is added to the print page renderer object. The second parameter in the
addPrintFormatter(...)
method is used to specify the starting page that the print formatter will apply. In our case we set the zero value, as we’re going to have one page only. - The actual drawing to PDF is taking place next. The
drawPDFUsingPrintPageRenderer(...)
is a custom method that we’ll define right after. The results of the drawing to PDF are stored to thepdfData
object, which is actually aNSData
object. - Saving the PDF data to a file is the next step. At first we specify the path to the file, giving at the same time the proper file name (it’s the
invoiceNumber
property). Then, we just write the PDF data to the that file. - The last step is obviously optional, but it’s useful here so we can find the newly created PDF file in Finder. When we’ll run the app, the path to the file will be shown to the console, so we’ll use it to open the PDF in Preview and verify the results.
In a more complicated app you can use multiple print formatter objects, and specify of course different starting pages for each formatter. But for now, the above is just fine for making our point clear.
Now, let’s pass to the implementation of the other custom method that will perform the actual drawing to a PDF context. As you’ll see next, Core Graphics techniques are used to achieve that, but the whole process is short and even self-explanatory. Let’s see it:
func drawPDFUsingPrintPageRenderer(printPageRenderer: UIPrintPageRenderer) -> NSData! { let data = NSMutableData() UIGraphicsBeginPDFContextToData(data, CGRectZero, nil) UIGraphicsBeginPDFPage() printPageRenderer.drawPageAtIndex(0, inRect: UIGraphicsGetPDFContextBounds()) UIGraphicsEndPDFContext() return data }
At first we initialise a mutable data object, so the PDF output data can be written to it. That’s something we actually dictate the code to do upon the creation of the PDF graphics context (second line). Next, we begin the creation of a new PDF page, but the actual drawing is taking place with that line:
printPageRenderer.drawPageAtIndex(0, inRect: UIGraphicsGetPDFContextBounds())
With that line, the print page renderer object that comes as an argument to the method will draw its contents inside the frame of the PDF context. Note that any custom header or footer will be automatically drawn as well, as the drawPageAtIndex(...)
calls all the other drawing methods of the print page renderer object.
Lastly, we close the PDF graphics context, while the drawing result data is returned back to the exportHTMLContentToPDF(...)
method that calls this one.
The above method prints a single page. However, in case you need to print multiple pages, then you can include the beginning of the PDF page creation and the print page renderer drawing into a loop. Keep that in mind in case you want to extend this demo app or make one of yours and you desire multiple pages to be printed in a single PDF file.
At that point all the tasks related to PDF export have come to their end. We’re not over yet, as in the next part we are going to see how to add custom header and footer to the printed page. However, and before doing that, let’s put all the above in action.
Open the PreviewViewController.swift file, and locate the exportToPDF(...)
IBAction method. Add the following line, so the previewed invoice to be exported to a PDF file when tapping on the PDF bar button item:
@IBAction func exportToPDF(sender: AnyObject) { invoiceComposer.exportHTMLContentToPDF(HTMLContent) }
You can now test the app, but for quick results I would suggest to do so in the Simulator. Once you select an invoice to preview, tap on the PDF bar button item to the top right side of the screen:
By doing so, the printing to PDF will take place, and when everything has finished, you’ll see the path to the output file to the console. Copy just the path (without the file name), and open a new Finder window. Use the Shift-Command-G key combination, and paste the path there. In the landing folder you’ll see the newly created PDF file using the invoice number as its name.
Double click on it to preview it in the… Preview app (or choose any other app you might like):
Drawing Custom Header and Footer
Let’s go now to extend a little bit more this example by adding custom content to the header and footer of the printed page. After all, that’s the reason we subclassed the UIPrintPageRenderer
class in first place. By saying custom content, I mean content that is not part of the HTML templates and is not rendered along with the rest of the HTML content. What we want to achieve is to add the word “Invoice” to the top-right side of the page (as a header), and the phrase “Thank you!” to the bottom side of the page (footer) with a horizontal line above it. The following screenshot will make our goal clear:
Before we see the details of the header and the footer, we must specify the desired height for the header and the footer. Open the CustomPrintPageRenderer.swift
file, and add the following two lines in the init()
method (note that both properties are from the UIPrintPageRenderer
class):
override init() { ... self.headerHeight = 50.0 self.footerHeight = 50.0 }
We’ll start working on the header of the page first. We are going to override the following method as it’s originally defined in the UIPrintPageRenderer class:
override func drawHeaderForPageAtIndex(pageIndex: Int, inRect headerRect: CGRect) { }
The steps that we’ll take in the body of that method are outlined right below:
- We’ll specify the text we want to draw to the header (the “Invoice” word).
- We’ll specify some properties regarding the text formatting, such as the font, the color and the distance between letters.
- We’ll calculate the size of the rectangle containing the text after having applied our formatting, and we’ll specify the offset from the right edge of the page.
- We’ll determine the starting drawing point of the text.
- We’ll draw the text (eventually).
Here’s what I just described converted to code. There are comments that will make it easier to understand each line:
override func drawHeaderForPageAtIndex(pageIndex: Int, inRect headerRect: CGRect) { // Specify the header text. let headerText: NSString = "Invoice" // Set the desired font. let font = UIFont(name: "AmericanTypewriter-Bold", size: 30.0) // Specify some text attributes we want to apply to the header text. let textAttributes = [NSFontAttributeName: font!, NSForegroundColorAttributeName: UIColor(red: 243.0/255, green: 82.0/255.0, blue: 30.0/255.0, alpha: 1.0), NSKernAttributeName: 7.5] // Calculate the text size. let textSize = getTextSize(headerText as String, font: nil, textAttributes: textAttributes) // Determine the offset to the right side. let offsetX: CGFloat = 20.0 // Specify the point that the text drawing should start from. let pointX = headerRect.size.width - textSize.width - offsetX let pointY = headerRect.size.height/2 - textSize.height/2 // Draw the header text. headerText.drawAtPoint(CGPointMake(pointX, pointY), withAttributes: textAttributes) }
There’s just one thing that I haven’t mentioned in the above code, and that is the use of the getTextSize(...)
method. As you correctly guess, it’s another custom method that calculates and returns the size of the text’s frame. That calculation is taking place in a separate method because we’re going to need it when we’ll draw the footer as well.
Here’s the getTextSize(...)
method:
func getTextSize(text: String, font: UIFont!, textAttributes: [String: AnyObject]! = nil) -> CGSize { let testLabel = UILabel(frame: CGRectMake(0.0, 0.0, self.paperRect.size.width, footerHeight)) if let attributes = textAttributes { testLabel.attributedText = NSAttributedString(string: text, attributes: attributes) } else { testLabel.text = text testLabel.font = font! } testLabel.sizeToFit() return testLabel.frame.size }
The above is a common tactic for calculating the size of the frame surrounding some text. By using a temporary label, we set either a simple text with font or an attributed text, and we use the sizeToFit()
method to let the system calculate the exact size for us.
Let’s go now to the footer of the page. The steps followed here are pretty much the same as above, so there’s no need for me to comment almost anything. Just note in the code below that the text is centred horizontally, the text color is different and that there’s no additional space between letters:
override func drawFooterForPageAtIndex(pageIndex: Int, inRect footerRect: CGRect) { let footerText: NSString = "Thank you!" let font = UIFont(name: "Noteworthy-Bold", size: 14.0) let textSize = getTextSize(footerText as String, font: font!) let centerX = footerRect.size.width/2 - textSize.width/2 let centerY = footerRect.origin.y + self.footerHeight/2 - textSize.height/2 let attributes = [NSFontAttributeName: font!, NSForegroundColorAttributeName: UIColor(red: 205.0/255.0, green: 205.0/255.0, blue: 205.0/255, alpha: 1.0)] footerText.drawAtPoint(CGPointMake(centerX, centerY), withAttributes: attributes) }
That code creates the “Thank you!” message, but it doesn’t add the line above it. Therefore, let’s make an addition to that method:
override func drawFooterForPageAtIndex(pageIndex: Int, inRect footerRect: CGRect) { ... // Draw a horizontal line. let lineOffsetX: CGFloat = 20.0 let context = UIGraphicsGetCurrentContext() CGContextSetRGBStrokeColor(context, 205.0/255.0, 205.0/255.0, 205.0/255, 1.0) CGContextMoveToPoint(context, lineOffsetX, footerRect.origin.y) CGContextAddLineToPoint(context, footerRect.size.width - lineOffsetX, footerRect.origin.y) CGContextStrokePath(context) }
Now we have a horizontal line as well!
Before reaching the end of this part, there’s one observation I’d like to make regarding both header and footer. If you notice, you’ll see that both texts are NSString
, and not String
objects. There’s a specific reason for that: The drawAtPoint(...)
method that performs the real drawing belongs to the NSString
class. If you want to use a String
object instead, then you have to cast it to NSString
as follows:
(text as! NSString).drawAtPoint(...)
Run the app again now and preview the exported PDF; this time contains a header and a footer.
Bonus Part: Previewing and Emailing the PDF
At this point we have already reached to the end of the main concept of this post. However, if you run the demo app on a device there’s no straight way to see the exported PDF (you can do it actually through Xcode but it’s a hassle to do it every time you create a PDF), so we’ll add two extra features in our app: The capability to preview the PDF in the web view that already exists in PreviewViewController
, and to send it via email. We’ll allow the user to select the final action by presenting an alert controller with all the possible options. We won’t stick too much to the details, as any code presented here is out of the scope of the tutorial.
Our work will take place in the PreviewViewController.swift
file, so find it in the Project Navigator and open it. Add the following new method that displays the alert controller to the screen:
func showOptionsAlert() { let alertController = UIAlertController(title: "Yeah!", message: "Your invoice has been successfully printed to a PDF file.\n\nWhat do you want to do now?", preferredStyle: UIAlertControllerStyle.Alert) let actionPreview = UIAlertAction(title: "Preview it", style: UIAlertActionStyle.Default) { (action) in } let actionEmail = UIAlertAction(title: "Send by Email", style: UIAlertActionStyle.Default) { (action) in } let actionNothing = UIAlertAction(title: "Nothing", style: UIAlertActionStyle.Default) { (action) in } alertController.addAction(actionPreview) alertController.addAction(actionEmail) alertController.addAction(actionNothing) presentViewController(alertController, animated: true, completion: nil) }
The actions for each option are not defined yet, so let’s do that now. For the preview action we’ll load the PDF file from the file into the web view using a NSURLRequest
object as shown below:
let actionPreview = UIAlertAction(title: "Preview it", style: UIAlertActionStyle.Default) { (action) in let request = NSURLRequest(URL: NSURL(string: self.invoiceComposer.pdfFilename)!) self.webPreview.loadRequest(request) }
For sending the email, let’s create a new method that will prepare it and attach the file to it:
func sendEmail() { if MFMailComposeViewController.canSendMail() { let mailComposeViewController = MFMailComposeViewController() mailComposeViewController.setSubject("Invoice") mailComposeViewController.addAttachmentData(NSData(contentsOfFile: invoiceComposer.pdfFilename)!, mimeType: "application/pdf", fileName: "Invoice") presentViewController(mailComposeViewController, animated: true, completion: nil) } }
Don’t forget to go to the top of the file and add the following line so you can use the MFMailComposeViewController
:
import MessageUI
Back to the showOptionsAlert()
method, let’s complete the actionPreview
action as shown in the next snippet:
let actionEmail = UIAlertAction(title: "Send by Email", style: UIAlertActionStyle.Default) { (action) in dispatch_async(dispatch_get_main_queue(), { self.sendEmail() }) }
We’re almost done, as there’s just one thing still missing. We must call the showOptionsAlert()
method. The alert controller should be presented to the user right after the PDF file has been exported to the documents directory, therefore go to the exportToPDF(...)
IBAction method and add the single line shown below:
@IBAction func exportToPDF(sender: AnyObject) { ... showOptionsAlert() }
That’s all! You can now run the app on a device and interact with the exported PDF file.
Summary
No matter what techniques currently exist or will exist in the future for creating PDF documents, the method presented in this post will always be a standard, flexible and secure way to render PDF files. It’s suitable for the most of the cases, with one drawback only: The need of HTML templates that will be used to generate the real content. For me, this is a low price for what we get as a payback. I strongly believe that it’s way more preferable to mess with HTML code, placeholders and string replacements than taking the long way of manually drawing a PDF. Other than that, the real drawing to PDF code is quite standard, and with a few tweaks to the demo app’s code you can have great results. In any case, I hope you like the technique described in this post and you actually use it in your projects. Thanks for reading, and happy printing to PDF documents!
For reference, you can refer to the complete Xcode project on Github.com.