April 18, 2020

Open a PDF file with Swift(PDFKit, WKWebView)

Open a PDF file with Swift(PDFKit, WKWebView)

PDF files are common place and with iOS/iPadOS becoming a more powerful operating system, opening PDF files and other regular computer type tasks will become more common place.

In this tutorial we explore two ways of opening a PDF file with Swift. The PDF can be a local file or a hosted file but we will be using a local file. If you want to use a hosted file it will work in basically the same way. Both methods that we are going to use today allows for us to use a URL when trying to load the PDF, so it doesn't matter whether the URL is a local file or not.

Even though this tutorial is not focussed on opening a remote PDF, I will be adding the code at the end of each section that will allow you to open a remote PDF file. You can also find the full source code for this tutorial here.

Now that we know what we are doing, let's get to the code:

Step 1: Get URL for local PDF

As I mentioned earlier in this tutorial we will be focussing on how to open a local PDF file. In order to do that we need to have a PDF file that we can add to our project. To do that I have used the following PDF file - https://web.stanford.edu/class/archive/cs/cs161/cs161.1168/lecture4.pdf. I have named this file heaps.pdf, so in the code below you will see that I use the file name heaps.

I will not be including the PDF file in the repo as I am not sure what license it has and if I am allowed to distribute it.

Because we are adding this PDF to our project we can use Bundle.main to get the file's URL. To do this we need to create the following method:

private func resourceUrl(forFileName fileName: String) -> URL? {
    if let resourceUrl = Bundle.main.url(forResource: fileName,
                                         withExtension: "pdf") {
        return resourceUrl
    }
    
    return nil
}

This method will take the file name as the argument. If the file exists then we will return the resourceUrl, if it does not exist we will return nil.

We will use this method for both the WKWebView and PDFKit.

NOTE: All the code for this tutorial will be in my ViewController file as I am working with a new project and this is a tutorial. If this wasn't a tutorial I would put this code in a more appropriate place.

Open PDF with WKWebView

Now that we have our resourceUrl method that will return the URL for our PDF file, we can implement our createWebView method. Just before we do that, we need to import WebKit.

Add the following import to your file:

import WebKit

Ok, now that we have WebKit imported, we can create our new method. This new method will create a new WKWebView and then it will load the URL and return the webview that was created. But, this will only happen if a PDF exists, if one doesn't then this method will return nil.

private func createWebView(withFrame frame: CGRect) -> WKWebView? {
    let webView = WKWebView(frame: frame)
    webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    
    if let resourceUrl = self.resourceUrl(forFileName: "heaps") {
        webView.loadFileURL(resourceUrl,
                            allowingReadAccessTo: resourceUrl)
        
        return webView
    }
    
    return nil
}

The above method takes a frame as an argument so that we can set the WKWebView frame when we initialise it.

After that we set the autoResizingMask with .flexibleWidth and .flexibleHeight.

Now we will use the resourceUrl method that we created in Step 1. We call the resourceUrl method with heaps as the fileName argument value, this is the name of my PDF file, please change this name to whatever you have called your PDF file. If resourceUrl returns a URL we load it in the webview by using loadFileUrl. If it returns nil, we will return nil because the webview will be empty.

If you want a full tutorial on loading local files with a WKWebView, I have written one that you can find here.

Displaying the webview

Now that we have created a webview to display the PDF, we need to show the webview. To do this we will create a new method called displayWebView, which will look like this:

private func displayWebView() {
    if let webView = self.createWebView(withFrame: self.view.bounds) {
        self.view.addSubview(webView)
    }
}

Nothing complicated here. We call the createWebView method with the our view's bounds. If this method returns a webview we will add it as a subview to self.view, if it returns nil then nothing will happen.

We can now display the webview by calling the displayWebView in our viewDidLoad method. Update your viewDidLoad method to look like the following:

override func viewDidLoad() {
    super.viewDidLoad()
    self.displayWebView()
}

If you build and run the app now you should see the following:

Open remote PDF with WKWebView

Loading a local PDF file into a WKWebView is easy, but loading one from a remote URL is even easier because we do not need to use the resourceUrl method. If you want to open the PDF from the url of the file that I linked in Step 1, you can update the createWebView method to the following:

private func createWebView(withFrame frame: CGRect) -> WKWebView? {
    let webView = WKWebView(frame: frame)
    webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    
    if let resourceUrl = URL(string: "https://web.stanford.edu/class/archive/cs/cs161/cs161.1168/lecture4.pdf") {
        let request = URLRequest(url: resourceUrl)
        webView.load(request)
        
        return webView
    }
    
    return nil
}

Instead of calling resourceUrl we used URL(string: ) which will return a URL if the string that we pass through is valid.

Opening a remote PDF cannot be done by using loadFileURL, because loadFileURL is only for local files. So in order to load a remote URL we need to create a URLRequest and pass the URLRequest to the WKWebView method called load.

If you build and run the app now, you will see that the PDF loads as expected, however, it will take longer because it needs to download the file.

Open PDF with PDFKit

If you skipped Step 1, please ensure that you read that before continuing. In Step 1 we created the resourceUrl method that will return the file URL.

When using PDFKit there are two main classes that we are going to use. These two classes are PDFView and PDFDocument.

PDFView is a subclass of UIView and it will be the view that shows the PDF file. PDFDocument loads the PDF file and allows us to set the document property on the PDFView.

Before we can use these classes we need to import PDFKit. To do that add the following import to your file:

import PDFKit

Now that we have imported PDFKit we can start creating the methods that will allow us to use it.

The first method we will create is the createPdfView. This method will take a frame as an argument. We will use this when we initialise our PDFView.

Add the following code to your file:

private func createPdfView(withFrame frame: CGRect) -> PDFView {
    let pdfView = PDFView(frame: frame)
    pdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    pdfView.autoScales = true
    
    return pdfView
}

This method will initialise a PDFView, it will set the autoResizingMask and then we will set autoScales as true. If we don't set the autoScales the scale of the PDF document won't be correct. If you want you can set this manually, but for this tutorial using autoScales is fine. Once the PDFView has be setup we return it.

We can now use the resourceUrl method and create a PDFDocument. To do that, add the following method to your code:

private func createPdfDocument(forFileName fileName: String) -> PDFDocument? {
    if let resourceUrl = self.resourceUrl(forFileName: fileName) {
        return PDFDocument(url: resourceUrl)
    }
    
    return nil
}

This method will take the file name of the file we want to open. We pass that file name to resourceUrl, if the file exists we create a PDFDocument, if it doesn't exist we will return nil.

All the hard work is now done. Let's create our display method. Add the following code:

private func displayPdf() {
    let pdfView = self.createPdfView(withFrame: self.view.bounds)
    
    if let pdfDocument = self.createPdfDocument(forFileName: "heaps") {
        self.view.addSubview(pdfView)
        pdfView.document = pdfDocument
    }
}

In this method we call createPdfView and we pass self.view.bounds to set the frame of the PDFView. Next, we call createPdfDocument in an if let. If the PDFDocument could be created, we will add pdfView as a subview to self.view and we will assign pdfDocument to pdfView.document so that when the view gets shown, the PDF will be visible.

The last thing that we need to do now is call displayPdf from our viewDidLoad. Update your viewDidLoad to the following:

override func viewDidLoad() {
    super.viewDidLoad()
    self.displayPdf()
}

If you build and run the app now you will see the following:

This looks almost identical when comparing it to the webview implementation. There is one visual difference besides the scaling. When using a PDFView, we don't have a page count in the top left corner, where the webview does.

Open remote PDF file with PDFKit

To open a remote PDF with PDFKit we need to change one line in our createPdfDocument method.

Update createPdfDocument method to look like this:

private func createPdfDocument(forFileName fileName: String) -> PDFDocument? {
    if let resourceUrl = URL(string: "https://web.stanford.edu/class/archive/cs/cs161/cs161.1168/lecture4.pdf") {
        return PDFDocument(url: resourceUrl)
    }
    
    return nil
}

The only change in the above method is removing the call to self.resourceUrl and replacing it with URL(string: ). If you build and run the app now, everything will work as expected, except it will take a bit longer for the PDF to be displayed because it needs to be downloaded.

Conclusion:

Either of these methods make it easy to open PDF files, whether the file is local to the device or not. Deciding on which to use is up to you. One way might suit your needs to better, it all depends what your requirements are.

If you want to see the full source, you can find it here.