In this article we describe how to add a PDF viewer to your iOS app using SwiftUI, Apple's declarative UI framework announced at WWDC 2019.

linkGetting started

Integrating the PDFTron iOS SDK for a project that uses SwiftUI is the same as for a "standard" Swift project. Please see our integration guide for more information.

In your SwiftUI project integrated with PDFTron iOS, create a new SwiftUI View file (Command+N -> Select "SwiftUI View") named DocumentView.swift and import the PDFNet and Tools modules at the top of the Swift file, below the existing SwiftUI import:

import SwiftUI

import PDFNet
import Tools

The DocumentView.swift file contains a placeholder View struct type that we will not use, so it can be removed. We can now declare an empty DocumentView struct type that conforms to the UIViewControllerRepresentable protocol. For this example, we will be wrapping the PTDocumentViewController class, a full-featured PDF and document viewer and annotator.

struct DocumentView : UIViewControllerRepresentable {
    
}

The UIViewControllerRepresentable protocol is required to create a SwiftUI View that represents a UIKit UIViewController. There are two required functions in the protocol that must be implemented, the makeUIViewController(context:) and updateUIViewController(_:context:) functions. The first function is called to give us a chance to create and return a view controller instance (a PTDocumentViewController), and the second is called to update the view controller when an update is necessary.

Add these two functions to the DocumentViewer struct with the following implementations:

func makeUIViewController(context: Context) -> PTDocumentViewController {
    // Create and return a new PTDocumentViewController instance.
    return PTDocumentViewController()
}

func updateUIViewController(_ uiViewController: PTDocumentViewController, context: Context) {
    // Empty.
}

It's now possible to display the SwiftUI-wrapped PTDocumentViewController from the UIWindowSceneDelegate lifecycle function scene(_:willConnectTo:options:). A basic implementation of this function looks like the following:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use a UIHostingController as window root view controller.
    let window = UIWindow(frame: UIScreen.main.bounds)

    window.rootViewController = UIHostingController(rootView: DocumentView())
    
    self.window = window
    window.makeKeyAndVisible()
}

The UIHostingController, a UIViewController subclass, is a class whose purpose is to wrap a SwiftUI View (a DocumentView in this case) for use in UIKit. This could be thought of as the opposite of the UIViewControllerRepresentable protocol: UIHostingController makes SwiftUI available from UIKit, and UIViewControllerRepresentable makes UIKit acessible from SwiftUI.

Running the project shows the following empty viewer:

Empty SwiftUI DocumentView.
An empty SwiftUI PDF viewer.

linkShowing a document

The next step is to load a document in the viewer. First, add a url property to the DocumentView struct, and use it to open a document in the makeUIViewController(context:) function:

struct DocumentView : UIViewControllerRepresentable {
    
    var url: URL?
    
    func makeUIViewController(context: Context) -> PTDocumentViewController {
        // Create and return a new PTDocumentViewController instance.
        let documentViewController = PTDocumentViewController()
        
        // Check if there was a URL specified.
        if let url = url {
            // Open the document at the specified URL.
            documentViewController.openDocument(with: url)
        }

        return documentViewController
    }

    // ...    
    
}

Then a URL that points to a document needs to be passed into the DocumentView created in the UIWindowSceneDelegate:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // ...

        // Create a URL for a remote PDF file.
        let url = URL(string: "https://pdftron.s3.amazonaws.com/downloads/pl/PDFTRON_mobile_about.pdf")

        // Pass the URL into the DocumentView struct's memberwise initializer.
        window.rootViewController = UIHostingController(rootView: DocumentView(url: url))

        // ...
    }

If you run the app, it will now look like this:

Working DocumentView.
A working SwiftUI PDF viewer.

linkWhere's the navigation bar?

Navigation stacks in SwiftUI are handled with a NavigationView view. While this view does contain a UINavigationController internally, it is not possible to populate its navigation bar with UIBarButtonItems from a SwiftUI-wrapped UIViewController. This is because the wrapped view controller's parent is a UIHostingController that is required for the SwiftUI to UIKit bridging. The UIHostingController is the view controller placed onto the (UIKit) navigation stack, not the SwiftUI-wrapped UIViewController. To work around this limitation, the PTDocumentViewController can be placed within a UINavigationController that is then placed within a SwiftUI view.

The PTDocumentViewController class uses the top navigation bar provided by a UINavigationController to display its buttons for searching, sharing, annotating, and more. Currently the DocumentView does not have a navigation bar but this can be added by wrapping the PTDocumentViewController instance in a UINavigationController:

func makeUIViewController(context: Context) -> UINavigationController {
    // ...
    
    let navigationController = UINavigationController(rootViewController: documentViewController)
    return navigationController
}

The type of the UIViewController parameter in the updateUIViewController(_:context:) method also needs to be updated to the UINavigationController class:

func updateUIViewController(_ navigationController: UINavigationController, context: Context) {
    // Empty.
}

Now the DocumentView will have a navigation bar displaying all the PTDocumentViewController's buttons:

Viewer with navigation bar.
The viewer with a navigation bar and buttons.

linkSafe areas

The default behavior of SwiftUI Views is to respect the edges of the safe area. However, for a fullscreen SwiftUI View it is necessary to ignore the edges of the safe area. This can be done at the time the DocumentView is created in the UIWindowSceneDelegate using the edgesIgnoringSafeArea(_:)View modifier:

PDFDocumentView(url: url)
    .edgesIgnoringSafeArea(.all)

The DocumentView will now extend all the way to the top and bottom of the screen.

linkPDFKit

For simple use-cases where performance, rendering, and available features are not as important, it is also possible to use Apple's PDFKit framework to add a basic PDF viewer. We'll briefly go over how to show a PDFView in a SwiftUI app.

First a new PDFKitView struct needs to be declared. To wrap a UIView instead of a UIViewController, the struct type needs to conform to the UIViewRepresentable protocol. In the makeUIView(context:) function a PDFView is created and returned, optionally opening a PDFDocument with the provided URL.

import SwiftUI
import PDFKit

struct PDFKitView : UIViewRepresentable {
    
    var url: URL?
    
    func makeUIView(context: Context) -> UIView {
        let pdfView = PDFView()

        if let url = url {
            pdfView.document = PDFDocument(url: url)
        }
        
        return pdfView
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        // Empty
    }
    
}

The PDFKitView can be used in the same way as the DocumentView by passing a document URL to the struct's memberwise initializer. However, only PDF documents can be opened by the PDFKit viewer. The PTDocumentViewController-based SwiftUI view can open PDFs, Office (Word, Powerpoint, Excel) and iWork (Pages, Keynote, Numbers) documents, image files, and more.

linkConclusion

In this article we showed how to add a PDF viewer to a SwiftUI app with PDFTron and PDFKit. We're excited by the release of SwiftUI and look forward to working more with the framework in the coming months.

If you have any questions about integrating PDFTron into your project, please feel free to contact us and we'll be more than happy to help!