Generating a PDF from a UIScrollView
PDF Generation of a UICollectionView.
There are plenty of situations where you might want to generate a PDF from a UIScrollView. If you search the internet, you’ll find lots of posts instructing you to change the frame of the scroll view to enclose all of its contents. This won’t work if you’re using AutoLayout. In any case, there’s a better way.
My first thought was to render the layer of the scroll view into some kind of Core Graphics context. That’s not wrong, but it’s tricky to get right. If you simply scroll, then render, scroll again, then render again, your Core Graphics viewport moves outside the visible area of the scroll view and gets clipped.
Note that for this example, I used some UIKit extension methods which don’t exist on OS X, but there are equivalent Core Graphics methods in for NSScrollView. You’ll see them called out with the UI prefix we all know and love. So, let’s get to it.
The first thing we need is the default origin and size of our pages. Since bounds always start at (0, 0) and the scroll view’s bounds give us the correct size for the visible area, we can just use that.
In the United States, a standard printed page is 8.5 inches by 11 inches, but when generating a PDF it’s simpler to keep the page size matching the visible area of the scroll view. We can let our printer software (such as the Preview app on OS X or the Printer app on iOS) do the scaling.
If we wanted to scale ourselves, we could multiply each of those numbers by 72, to get the number of points for each dimension. We would have to change how we generated the the pages below, so for simplicity, we’re going to stick to one page per screenful of content.
let pageDimensions = scrollview.bounds
Now we need to know how many pages we will need to fit our content. To get this, we divide our scroll views dimensions by the size of each page, in either direction. We also need to round up, so that the pages don’t get clipped:
let pageSize = pageDimensions.size
let totalSize = scrollview.contentSize
let numberOfPagesThatFitHorizontally = Int(ceil(totalSize.width / pageSize.width))
let numberOfPagesThatFitVertically = Int(ceil(totalSize.height / pageSize.height))
Next, we have to set up a Core Graphics PDF context. First we create a backing store for the PDF data, then pass it and the page dimensions to Core Graphics.
let outputData = NSMutableData()
UIGraphicsBeginPDFContextToData(outputData, pageDimensions, nil)
We could pass in some document information here, which mostly cover PDF metadata, including author name, creator name (our software) and a password to require when viewing the PDF file.
Also note that we can use UIGraphicsBeginPDFContextToFile() instead, which writes the PDF to a specified path. I haven’t played with it, so I don’t know if the data is written all at once, or as each page is closed.
The last thing we need to do before generating our PDF is to save some state. Since we will be scrolling, we will be changing the offsets of the scroll view. We also need to clear the content insets, so that our core graphics layer and our content offset match up.
let savedContentOffset = scrollview.contentOffset
let savedContentInset = scrollview.contentInset
scrollview.contentInset = UIEdgeInsetsZero
We don’t need to reset the content offset, because that happens implicitly, in the loop below:
if let context = UIGraphicsGetCurrentContext()
for indexHorizontal in 0 ..< numberOfPagesThatFitHorizontally
for indexVertical in 0 ..< numberOfPagesThatFitVertically
let offsetHorizontal = CGFloat(indexHorizontal) * pageSize.width
let offsetVertical = CGFloat(indexVertical) * pageSize.height
scrollview.contentOffset = CGPointMake(offsetHorizontal, offsetVertical)
CGContextTranslateCTM(context, -offsetHorizontal, -offsetVertical)
The magic happens when we change the contentOffset and translate the current transformation matrix (CTM) of the Core Graphics PDF context.
Consider that the viewport of the core graphics context is attached to the top of the scroll view’s content view and we need to push it in the opposite direction as we scroll. Further, anything not inside of the visible area of the scroll view is clipped, so scrolling will move the core graphics viewport out of the rendered area, producing empty pages.
To counter this, we scroll the next screenful into view, and adjust the core graphics context. Note that core graphics uses a coordinate system which has the y coordinate decreasing as we go from top to bottom. This is the opposite of UIKit (although it matches AppKit on OS X.)
There are faster ways to snapshot a view into a UIImage, but view.layer.renderInContext is the most straightforward way to do what it says on the tin: render a layer into a context.
Finally, we need to close the PDF context or we’ll get an empty PDF.
We also want to put things back the way we found them in the view hierarchy.
scrollview.contentInset = savedContentInset
scrollview.contentOffset = savedContentOffset
By now, outputData contains a PDF document with screenfuls of data from the UIScrollView. This works with UIScrollView subclasses UITableView and UICollectionView as well.
You can get the sample project from on GitHub. Take a look at the file called “ScrollViewSnapshotter.swift” which contains all of the information in this post. Next time you need to snapshot a scroll view, don’t mess with the frame. Do it the right way!