Mastodon

Getting started with PDFKit

In my post about my custom presentation slide app I mentioned that because of time constraints, I decided against doing my own slide layout. Instead I’d do that in DeckSet, export my slides to PDF, and show the PDFs in my app.

So, how do you do that then?

Getting a PDF on the screen

The basics of PDFKit are actually pretty basic. If you have a PDF, you create a PDFDocument to hold it and a PDFView to display it. Put that view on the screen and there it is. It can be as simple as this:

if let pdfDocument = PDFDocument(url: documentURL) {
    pdfView = PDFView(frame: view.bounds)
    pdfView.document = pdfDocument
    view.addSubviewAndConstrain(pdfView)
}

Sharp eyed readers might wonder about that last line. It’s a convenience I often use when I want to add a new subview and have the subview completely fill the parent. It supplements addSubView(_:) with code to create some useful autolayout constraints:

extension UIView {
    func addSubviewAndConstrain(_ subview: UIView) -> Void {
        subview.frame = self.bounds
        subview.translatesAutoresizingMaskIntoConstraints = false
        
        subview.alpha = 1.0
        self.addSubview(subview)
        
        NSLayoutConstraint.activate([
            self.widthAnchor.constraint(equalTo: subview.widthAnchor, multiplier: 1.0),
            self.heightAnchor.constraint(equalTo: subview.heightAnchor, multiplier: 1.0),
            self.leadingAnchor.constraint(equalTo: subview.leadingAnchor),
            self.trailingAnchor.constraint(equalTo: subview.trailingAnchor),
            self.topAnchor.constraint(equalTo: subview.topAnchor),
            self.bottomAnchor.constraint(equalTo: subview.bottomAnchor)
            ])
    }
}

So that’s… functional, I guess. But the PDF doesn’t fit the screen or work like I want a slide presentation app to work. Fortunately PDFView has some handy options to customize its behavior. Since I’m using it to show presentation slides, what I want is

  • A PDF page should fill the parent view, so that it fills the screen.
  • I want to swipe horizontally between slides.
  • Exactly one full page should show at a time, so when I swipe I should end up with a complete slide.
  • If the PDF doesn’t fit the screen aspect ratio, the background should be dark so it doesn’t stand out.

PDFView has me covered:

    pdfView.displayMode = .singlePage
    pdfView.displayDirection = .horizontal
    pdfView.autoScales = true
    pdfView.usePageViewController(true, withViewOptions: nil)
    pdfView.backgroundColor = .black

Improving slide navigation

That covers the basic need of showing my slides, making them look good, and easily navigating from one slide to the next or previous one. I realized I needed one more thing. Anyone who’s done a presentation knows that, however much you plan, at some point you may need to jump to a different slide, out of order. Often it’s during Q&A. In my case I was hoping for a lot of audience participation so I wanted to make this easy.

For this I turned to PDFThumbnailView, which as its name suggests, shows thumbnail images of a PDF. It works with a PDFView, so tapping on a thumbnail updates the PDFView state, and changing pages in a PDFView updates the thumbnail display. Setting it up basically involves telling it what size thumbnails you want and what PDFView to work with.

    let thumbnailSize: Int = 150

    pdfThumbnailView = PDFThumbnailView()
    pdfThumbnailView.translatesAutoresizingMaskIntoConstraints = false
    pdfThumbnailView.pdfView = pdfView
    pdfThumbnailView.layoutMode = .horizontal
    pdfThumbnailView.thumbnailSize = CGSize(width: thumbnailSize, height: thumbnailSize)

    view.addSubview(pdfThumbnailView)

This sets up a horizontal thumbnail view. The thumbnail size above is arbitrary, just a size that looked good to me.

Then there’s some typical layout and appearance stuff that you’d have with any UIView:

    NSLayoutConstraint.activate([
        pdfThumbnailView.heightAnchor.constraint(equalToConstant: CGFloat(thumbnailSize)),
        pdfThumbnailView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        pdfThumbnailView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        pdfThumbnailView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
    ])
    
    pdfThumbnailView.backgroundColor = .clear

That puts the horizontal thumbnail view across the bottom of the enclosing view, which means it’s in front of the PDFView. I didn’t use addSubviewAndConstrain here because I don’t want the thumbnail to fill the view, I want it to run along the bottom.

It ends up looking like this (using a demo slide PDF that just shows the current page number in large text):

Keeping those Slides Visible

I don’t want this to be there all the time though, so I need some way to make it show and hide. Initially I’ll make it hidden:

pdfThumbnailView.alpha = 0

Then I’ll add a gesture recognizer to the PDFView, so that tapping on it triggers an action:

// Add tap gesture to show/hide thumbnails
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(pdfViewTapped))
pdfView.addGestureRecognizer(tapGestureRecognizer)

And finally I’ll make that action smoothly make the thumbnail fade in and out:

@objc func pdfViewTapped() -> Void {
    let newAlpha: CGFloat = {
        if thumbnailContainerView.alpha < 0.5 {
            return 1.0
        } else {
            return 0.0
        }
    }()
    UIView.animate(withDuration: 0.3) {
        self.pdfThumbnailView.alpha = newAlpha
    }
}

I don’t store any state about whether the thumbnail view is visible or hidden. Instead I check on the current UI and make the appropriate change. Now, I can tap on the PDF view and the thumbnails appear and disappear as needed.

Next Steps

You might notice that the thumbnail screenshot doesn’t show every slide. It’s a 50 slide test presentation but there are only a few thumbnails. That comes down to my choice of thumbnail size, as compared to screen size, combined with the fact that PDFThumbnailView does not scroll. Presentation apps generally show thumbnails of every slide though, and that’s what I wanted. I’ll follow up with another post where I explain why this is happening, and what I did about it.

You can find that followup here.