Customize the Display of PDFs with the View Hierarchy
This guide discusses the view hierarchy used by PSPDFKit to display documents and the API that can be used to customize the presentation of documents.
Getting Started
Controlling the Document
There are a few simple convenience methods available for you on PDFViewController
. These cover the most common tasks while hiding the underlying complexity of documents and how they are visualized onscreen. For basic controls such as changing a page, these APIs might be all you need. However, if you want to have more fine-grained control, PDFDocumentViewController
(opens in a new tab) offers the full set of APIs and can be accessed via PDFViewController
’s documentViewController
(opens in a new tab) property.
Pages vs. Spreads
The most fundamental difference between the methods on PDFViewController
and PDFDocumentViewController
(opens in a new tab) is that the main view controller deals with pages and page indexes, whereas the document view controller deals with spreads and spread indexes. A spread is a collection of pages that are always viewed together. If you have the most basic layout — a layout with its spread mode set to single — every spread corresponds to a single page in the document, which means the number of spreads is equal to the number of pages.
However, you can also have a double-page layout or a book layout. PDFDocumentViewLayout
(opens in a new tab) provides methods to convert between pages and spreads: spreadIndexForPage(at:)
(opens in a new tab) and pageRangeForSpread(at:)
(opens in a new tab). To make your code independent of the layout you use, use these methods for conversion, and in case you want to implement your own spreading algorithms, override these methods and make them do the proper conversion. Otherwise, your layout may not work correctly in all cases.
Because a spread can have multiple pages, there is not always a single page presented to the user — imagine a book where the reader is not looking at a single page but instead at two pages at a time. Using a method like PDFViewController.pageIndex
(opens in a new tab) doesn’t give you the full picture. While this is very convenient and works for many scenarios, keep in mind that the returned page might only be one of multiple pages. If you need to get the full picture, use PDFDocumentViewController.spreadIndex
(opens in a new tab).
The number of pages per spread can vary. If you’re using PSPDFDocumentViewLayoutSpreadMode.book
(opens in a new tab), the first page will be treated as a cover and therefore will always be the only page in the first spread, while the following pages will be grouped together in groups of two. Also, when using PSPDFDocumentViewLayoutSpreadMode.book
(opens in a new tab) or PSPDFDocumentViewLayoutSpreadMode.double
(opens in a new tab), the last spread might only contain one page, depending on the number of pages in the document.
Customizing the Layout
There are two sets of APIs to support customizing the layout of a document. The high-level one is modeled by PDFConfiguration
and contains the very basic customization options. This should be enough for most projects. However, if you want to have more control over the layout, PDFDocumentViewController
(opens in a new tab) is available on PDFViewController
. This is the go-to API for making detailed changes to how the layout works.
These two concepts cannot be mixed and matched. Either you alter the layout through PDFConfiguration
, or you set your own layout instance on PDFDocumentViewController
(opens in a new tab). The options in the configuration only have an effect if the document view controller derives the layout from the configuration object. As soon as you set your own layout, the document view controller stops monitoring the configuration for layout-related properties. This is also true the other way around: While a layout is derived from the configuration, the document view controller might refresh this layout and update it at any given time, so you should not change properties on that layout, as these changes could be overridden at any time. As a general rule, only modify properties of layout objects that you created yourself.
For configuring your layout through PDFConfiguration
, check out the documentation of that class. This guide will mostly deal with how to implement your own layouts.
Subclassing
PDFDocumentViewLayout
(opens in a new tab) is an abstract base class meant for subclassing. You can subclass ContinuousScrollingLayout
(opens in a new tab) or ScrollPerSpreadLayout
(opens in a new tab) if you want a layout similar to these two with only a few tweaks. If you want to have more control but your layout still follows the general idea of a layout that scrolls in a single direction — either vertically or horizontally — StackViewLayout
(opens in a new tab), the superclass of the two aforementioned layouts, gives you a lot of control while covering the basics. It also provides some convenience methods that make your life easier. This should be enough control for almost all designs. However, if your design is very specific, we also give you the same base class all of our own layouts use: PDFDocumentViewLayout
(opens in a new tab). With this layout, you need to do everything on your own, but there are almost no limitations.
Aside from customizing the layout, you can also customize other parts of the view hierarchy. The document view controller, the spread view, and the page view all allow subclassing, which can be used to customize the experience even further. For example, you could customize PDFSpreadView
(opens in a new tab) and precisely control the frame of each page view inside it.
UICollectionViewLayout
A document view layout is based on UICollectionViewLayout
, and this is what is used to calculate the position of a spread on the screen. PDFDocumentViewLayout
(opens in a new tab) — and especially its subclass StackViewLayout
(opens in a new tab) — offer you convenience methods that hide a lot of the complexity of collection views, but depending on how the layout you are building should look, keep in mind that all the collection view layout methods can be used as well.
While the collection view layout works with index paths as its item identifying object, the document view layout does not need multiple levels of indexes; each layout only needs to deal with one section. The important identifier for a document view layout is the spread index of the item it is representing. Therefore, all the methods a PDFDocumentViewLayout
(opens in a new tab) and its subclasses offer refer to the spread index instead of an index path. For most layouts, you will not come into contact with any of the index path-based methods, but in case you do need them, PSPDFKit offers two new methods on NSIndexPath
for easily converting between index paths and spread indexes: NSIndexPath.pspdf_indexPathForSpread(at:)
(opens in a new tab) and NSIndexPath.pspdf_spreadIndex
(opens in a new tab). You should always use these methods to convert back and forth between spread indexes and index paths and not make any assumptions about how an index path maps to a spread index.
Additional APIs
Aside from the collection view layout, a document view layout also offers a few additional things that the document view controller and its views use to determine other behaviors, such as how zooming behaves (spreadBasedZooming
(opens in a new tab)), how spreads map to pages, and how the actual view hierarchy is positioned in relation to the view controller’s view (scrollViewFrameInsets
(opens in a new tab)).
Using the methods that PDFDocumentViewLayout
(opens in a new tab) and its subclasses offer is recommended over overriding the collection view layout methods. If you override one of the collection view layout methods, it’s up to you to make sure that other methods such as spreadIndexForPage(at:)
(opens in a new tab), pageRangeForSpread(at:)
(opens in a new tab), continuousSpreadIndex(forContentOffset:)
(opens in a new tab), and contentOffset(forContinuousSpreadIndex:)
(opens in a new tab) don’t return conflicting values — otherwise, you might get unexpected results.
Scrolling and Zooming
This is what the view hierarchy looks like:
There are two levels of scroll views. The outer scroll view is always the one that is responsible for scrolling (i.e. changing between pages). If the layout’s spreadBasedZooming
(opens in a new tab) is NO
, the outer scroll view is also responsible for zooming. Otherwise, this is taken over by the inner scroll view. The difference is that each spread view is contained in its own inner scroll view. Switching spreadBasedZooming
(opens in a new tab) controls if the user zooms the full document all the time (this behavior can be seen in the continuous scrolling layout) or if the user only zooms a single spread, leaving the zoom level of the other pages unchanged (this behavior can be seen in the scroll per spread layout).
All the positions that PDFDocumentViewLayout
(opens in a new tab) calculates are the positions that the inner scroll views (and therefore the spread views) have in the outer scroll view. To picture this, think about the layout as the thing that calculates which spread goes where, with the exception that every spread is then wrapped with a scroll view (the inner scroll view) to make it zoomable in spread-based zooming mode.
Callbacks
The PDFViewController
has a delegate, PDFViewControllerDelegate
(opens in a new tab), which receives callbacks on events related to the page view, annotations, text selections, document handling, etc. You will have to use an object conforming to the PDFViewControllerDelegate
(opens in a new tab) protocol and assign it to the delegate
property of PDFViewController
(opens in a new tab). Similarly, PDFDocumentViewController
(opens in a new tab) also has a delegate
property(opens in a new tab) of type PDFDocumentViewControllerDelegate
(opens in a new tab). This delegate receives callbacks for events related to the changes and configuration of the spread view. You can also use NSNotificationCenter
to observe the notifications of the spread view changes sent by PDFDocumentViewController
(opens in a new tab), the list of which can be found in the table below. You’ll have to use PSPDFDocumentViewControllerSpreadViewKey
(opens in a new tab) to access the spread view object sent in the user info dictionary of the notification. If you wish to perform actions or listen to the events of the document being scrolled, you can also assign custom delegates to the UIScrollView
objects received in the PDFDocumentViewControllerDelegate
(opens in a new tab) callbacks. You can check out DisableScrollBouncingExample.swift
(opens in a new tab) in the PSPDFKit Catalog project(opens in a new tab) to see how this can be achieved.
Insets
When used in the right combination, changing insets can be a very simple and powerful way to customize your layout. scrollViewFrameInsets
(opens in a new tab) determines the insets the outer scroll view’s frame gets relative to the document view controller’s view. StackViewLayout
(opens in a new tab) also offers contentInsets
(opens in a new tab), which is used to inset the positions of the actual items inside the outer scroll view. These two are what makes it possible for the scroll per spread layout to make the outer scroll view paginated but still show a gap between two spreads. It uses scrollViewFrameInsets
(opens in a new tab) to expand the scroll view’s frame to make the pagination size bigger and then uses contentInsets
(opens in a new tab) to move the actual spread views into the correct place.
Changing the Spacing between Pages
Here’s a basic example that removes the gap between pages using the interitemSpacing
(opens in a new tab) property of the built-in StackViewLayout
(opens in a new tab):
(pdfViewController.documentViewController?.layout as? StackViewLayout)?.interitemSpacing = 0
However, the documentViewController
might be recreated (e.g. this property will be nil
when opening an invalid or locked document), and its layout might change (e.g. using PDFSettingsViewController
(opens in a new tab)). A more robust solution that handles these changes is to subclass PDFViewController
and use key-value observing:
class NoGapPDFViewController: PDFViewController {
private var documentViewControllerLayoutObservation: NSKeyValueObservation?
override func documentViewControllerDidLoad() { super.documentViewControllerDidLoad()
documentViewControllerLayoutObservation = observe(\.documentViewController?.layout, options: [.initial, .new], changeHandler: { _, change in (change.newValue as? StackViewLayout)?.interitemSpacing = 0 }) }}