Creating a SwiftUI PDF Viewer App

In the world of mobile development, displaying PDF documents within an app is a common requirement. For iOS development using Swift, SwiftUI offers a declarative way to create user interfaces, including a PDF viewer. In this comprehensive guide, we will explore how to create a SwiftUI PDF viewer app, leveraging the power of PDFKit and UIKit integration.

What is SwiftUI?

SwiftUI is Apple’s modern UI framework for building user interfaces across all Apple platforms. It provides a declarative syntax that makes code easier to read and write. SwiftUI works seamlessly with existing UIKit code, allowing developers to integrate newer SwiftUI components into older UIKit-based apps, and vice versa.

Why Use SwiftUI for PDF Viewer?

  • Declarative Syntax: Easier to reason about and maintain UI code.
  • Live Preview: Instant feedback during development.
  • Cross-Platform Compatibility: Build apps for iOS, macOS, watchOS, and tvOS with shared code.
  • Integration with UIKit: Reuse existing PDFKit functionalities within SwiftUI.

Setting up a SwiftUI PDF Viewer App

To start, create a new Xcode project with the SwiftUI interface.

Step 1: Create a New Xcode Project

  • Open Xcode and select “Create a new Xcode project.”
  • Choose “iOS” and then “App.”
  • Give your project a name, e.g., “SwiftUI PDF Viewer,” and select “SwiftUI” as the interface.
  • Choose a location to save your project and click “Create.”

Step 2: Import Necessary Frameworks

Import PDFKit into your SwiftUI view. PDFKit is a framework that provides features to display, create, and manipulate PDF documents.

import SwiftUI
import PDFKit

Step 3: Create a PDFKitView Representable

Since PDFKit is part of UIKit, we need to create a UIViewRepresentable for SwiftUI to work with UIKit views. The PDFKitView will handle the display of the PDF document.

import SwiftUI
import PDFKit

struct PDFKitView: UIViewRepresentable {
    let url: URL

    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        pdfView.document = PDFDocument(url: url)
        pdfView.autoScales = true  // Enable auto scaling for better viewing
        return pdfView
    }

    func updateUIView(_ uiView: PDFView, context: Context) {
        uiView.document = PDFDocument(url: url)
    }
}

In this code:

  • PDFKitView conforms to UIViewRepresentable, making it compatible with SwiftUI.
  • makeUIView(context:) creates and configures the PDFView with the PDF document loaded from the specified URL.
  • updateUIView(_:context:) updates the PDFView if the URL changes.
  • autoScales = true enables auto scaling, ensuring that the PDF fits nicely within the view.

Step 4: Embed PDFKitView in SwiftUI View

Now, create a SwiftUI view that uses the PDFKitView to display the PDF document.

struct ContentView: View {
    let pdfURL = Bundle.main.url(forResource: "sample", withExtension: "pdf")!  // Replace "sample.pdf" with your PDF file name

    var body: some View {
        PDFKitView(url: pdfURL)
            .navigationTitle("PDF Viewer")
    }
}

In this code:

  • ContentView is a SwiftUI view that holds the PDFKitView.
  • pdfURL loads a sample PDF from the app’s bundle. You should replace "sample.pdf" with the name of your PDF file.
  • PDFKitView is initialized with the URL of the PDF document.
  • navigationTitle sets the title of the navigation bar.

Step 5: Add PDF File to Project

Add a PDF file to your Xcode project.

  • Drag and drop your PDF file (e.g., sample.pdf) into the Xcode project navigator.
  • Make sure “Copy items if needed” is checked.
  • Ensure the PDF is included in your app’s target.

Step 6: Enable Navigation in App File

Update the App file to embed the ContentView in a NavigationView, which will display the title.

@main
struct PDF_ViewerApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationView {
                ContentView()
            }
        }
    }
}

Customizing the PDF Viewer

You can add more features to enhance the PDF viewer app, such as zooming, scrolling, and searching.

Adding Zoom Functionality

PDFKit’s PDFView supports zooming. However, to control the zoom level via SwiftUI, you might use a @State variable and a slider.

import SwiftUI
import PDFKit

struct PDFKitView: UIViewRepresentable {
    let url: URL
    @Binding var zoomScale: CGFloat  // Add zoomScale binding

    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        pdfView.document = PDFDocument(url: url)
        pdfView.autoScales = false  // Disable auto scaling to manage zoom manually
        return pdfView
    }

    func updateUIView(_ uiView: PDFView, context: Context) {
        uiView.document = PDFDocument(url: url)
        uiView.scale(to: zoomScale, anchor: CGPoint(x: 0, y: 0))  // Set the zoom scale
    }
}

struct ContentView: View {
    let pdfURL = Bundle.main.url(forResource: "sample", withExtension: "pdf")!
    @State private var zoomScale: CGFloat = 1.0  // Initial zoom scale

    var body: some View {
        VStack {
            PDFKitView(url: pdfURL, zoomScale: $zoomScale)
                .navigationTitle("PDF Viewer")
            
            Slider(value: $zoomScale, in: 0.5...3.0, step: 0.1)  // Zoom slider
                .padding()
        }
    }
}

Key improvements and explanations:

  • @Binding var zoomScale: CGFloat in PDFKitView allows you to control the zoom level from the parent view (ContentView).
  • autoScales = false is set in makeUIView because you will be managing the scaling manually.
  • uiView.scale(to: zoomScale, anchor: CGPoint(x: 0, y: 0)) applies the zoom level to the PDFView.
  • @State private var zoomScale: CGFloat = 1.0 in ContentView initializes and holds the zoom level.
  • A Slider is added to the ContentView to control the zoom level.

Adding Page Navigation

Add buttons to navigate between pages in the PDF.

import SwiftUI
import PDFKit

struct PDFKitView: UIViewRepresentable {
    let url: URL
    @Binding var currentPage: Int
    @Binding var document: PDFDocument?

    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        if let document = PDFDocument(url: url) {
            pdfView.document = document
            self.document = document
        }
        pdfView.autoScales = true
        return pdfView
    }

    func updateUIView(_ uiView: PDFView, context: Context) {
        guard let document = document else { return }
        if currentPage >= 1 && currentPage <= document.pageCount {
            if let page = document.page(at: currentPage - 1) {
                uiView.goTo(page)
            }
        }
    }
}

struct ContentView: View {
    let pdfURL = Bundle.main.url(forResource: "sample", withExtension: "pdf")!
    @State private var currentPage: Int = 1
    @State private var document: PDFDocument? = nil

    var body: some View {
        VStack {
            HStack {
                Button(action: {
                    if currentPage > 1 {
                        currentPage -= 1
                    }
                }) {
                    Text("Previous")
                }
                Text("Page \(currentPage)")
                Button(action: {
                    if let document = document, currentPage < document.pageCount {
                        currentPage += 1
                    }
                }) {
                    Text("Next")
                }
            }
            .padding()

            PDFKitView(url: pdfURL, currentPage: $currentPage, document: $document)
                .navigationTitle("PDF Viewer")
        }
    }
}

Improvements and explanations:

  • @Binding var currentPage: Int and @Binding var document: PDFDocument? are added to PDFKitView to manage and update the current page and document.
  • In updateUIView, the current page is updated, ensuring it is within the valid page range.
  • Previous and Next buttons are added in ContentView to navigate between pages.
  • The currentPage state variable holds the current page number, and the document is stored so you can get total page count.

Handling PDF Loading Errors

Implement error handling to gracefully handle PDF loading failures.

import SwiftUI
import PDFKit

struct PDFKitView: UIViewRepresentable {
    let url: URL
    @Binding var pdfLoadError: Bool

    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        if let document = PDFDocument(url: url) {
            pdfView.document = document
            pdfView.autoScales = true
            pdfLoadError = false // Reset error if loaded successfully
        } else {
            pdfLoadError = true  // Set error if PDFDocument fails
        }
        return pdfView
    }

    func updateUIView(_ uiView: PDFView, context: Context) {
        uiView.document = PDFDocument(url: url)
    }
}

struct ContentView: View {
    let pdfURL = Bundle.main.url(forResource: "sample", withExtension: "pdf")!
    @State private var pdfLoadError: Bool = false

    var body: some View {
        VStack {
            if pdfLoadError {
                Text("Failed to load PDF. Please try again.")
                    .foregroundColor(.red)
                    .padding()
            } else {
                PDFKitView(url: pdfURL, pdfLoadError: $pdfLoadError)
                    .navigationTitle("PDF Viewer")
            }
        }
    }
}

Key changes:

  • @Binding var pdfLoadError: Bool in PDFKitView is used to report if there's a PDF loading error.
  • The makeUIView function now sets pdfLoadError based on whether the PDF loads successfully.
  • In ContentView, the app checks pdfLoadError and displays an error message if needed.

Conclusion

Creating a SwiftUI PDF viewer app involves integrating PDFKit through UIViewRepresentable. With SwiftUI's declarative syntax and PDFKit’s robust functionalities, you can build a feature-rich and efficient PDF viewer app. This comprehensive guide covered the basics of setting up a PDF viewer, adding zooming, implementing page navigation, and handling loading errors, giving you a solid foundation for more advanced features.