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 offers the full set of APIs and can be accessed via PDFViewController’s documentViewController property.

Pages vs. Spreads

The most fundamental difference between the methods on PDFViewController and PDFDocumentViewController 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 provides methods to convert between pages and spreads: spreadIndexForPage(at:) and pageRangeForSpread(at:). 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 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.

The number of pages per spread can vary. If you’re using PSPDFDocumentViewLayoutSpreadMode.book, 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 or PSPDFDocumentViewLayoutSpreadMode.double, 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 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. 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, please check out the documentation of that class. This guide will mostly deal with how to implement your own layouts.

Subclassing

PDFDocumentViewLayout is an abstract base class meant for subclassing. You can subclass ContinuousScrollingLayout or ScrollPerSpreadLayout 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, 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. 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 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 — and especially its subclass StackViewLayout — 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 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:) and NSIndexPath.pspdf_spreadIndex. 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), how spreads map to pages, and how the actual view hierarchy is positioned in relation to the view controller’s view (scrollViewFrameInsets).

Using the methods that PDFDocumentViewLayout 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:), pageRangeForSpread(at:), continuousSpreadIndex(forContentOffset:), and contentOffset(forContinuousSpreadIndex:) don’t return conflicting values — otherwise, you might get unexpected results.

Scrolling and Zooming

This is what the view hierarchy looks like:

Diagram showing the view hierarchy

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 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 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 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, 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 protocol and assign it to the delegate property of PDFViewController. Similarly, PDFDocumentViewController also has a delegate property of type PDFDocumentViewControllerDelegate. 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, the list of which can be found in the table below. You’ll have to use PSPDFDocumentViewControllerSpreadViewKey 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 callbacks. You can check out DisableScrollBouncingExample.swift in the PSPDFKit Catalog project 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 determines the insets the outer scroll view’s frame gets relative to the document view controller’s view. StackViewLayout also offers contentInsets, 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 to expand the scroll view’s frame to make the pagination size bigger and then uses contentInsets 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 property of the built-in StackViewLayout:

(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). 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
        })
    }
}