Show PDF Inside SwiftUI – PDF Viewer

SwiftUI does not have a direct PDF view like an Text or Image view. We can use PDFView from PDFKit. We have to wrap up this PDFView using UIViewRepresentable and then we will able to show PDFs inside SwiftUI.

Here is the SwiftUI wrapper for PDFView:

struct PDFViewer: UIViewRepresentable {
    let url: URL
    
    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        pdfView.autoScales = true
        return pdfView
    }
    
    func updateUIView(_ pdfView: PDFView, context: Context) {
        if let document = PDFDocument(url: url) {
            pdfView.document = document
        }
    }
}

Complete code:

import SwiftUI
import PDFKit

// Step 1: Create a custom UIViewRepresentable
struct PDFViewer: UIViewRepresentable {
    let url: URL
    
    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        pdfView.autoScales = true
        return pdfView
    }
    
    func updateUIView(_ pdfView: PDFView, context: Context) {
        if let document = PDFDocument(url: url) {
            pdfView.document = document
        }
    }
}

// Step 2: Use the custom PDFViewer in your SwiftUI view
struct ContentView: View {
    var body: some View {
        VStack {
            // You need to add sample pdf in the app bundle.
            if let pdfURL = Bundle.main.url(forResource: "sample", withExtension: "pdf") {
                PDFViewer(url: pdfURL)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
            } else {
                Text("PDF not found")
            }
        }
        .padding()
    }
}

// Preview
#Preview {
    ContentView()
}

The output will be:

Loading PDF from a Remote URL:

import SwiftUI
import PDFKit

struct PDFViewer: UIViewRepresentable {
    let url: URL
    
    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        pdfView.autoScales = true
        loadPDF(pdfView: pdfView)
        return pdfView
    }
    
    func updateUIView(_ uiView: PDFView, context: Context) {}
    
    private func loadPDF(pdfView: PDFView) {
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let data = data {
                DispatchQueue.main.async {
                    pdfView.document = PDFDocument(data: data)
                }
            }
        }.resume()
    }
}

struct ContentView: View {
    @State private var isLoading = false
    let pdfURL = URL(string: "https://pdfobject.com/pdf/sample.pdf")!
    
    var body: some View {
        PDFViewer(url: pdfURL)
            .edgesIgnoringSafeArea(.all)
    }
}
#Preview {
    ContentView()
}

Adding Controls in PDF Viewer:

To add controls to your PDF viewer, you can create custom buttons and overlay them on top of the PDFViewer. Here’s an example of how to add basic controls for zooming, going to the next/previous page, and displaying the current page number:

import SwiftUI
import PDFKit

struct PDFViewer: UIViewRepresentable {
    let document: PDFDocument?
    @Binding var currentPage: Int
    @Binding var totalPages: Int
    
    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        pdfView.autoScales = true
        pdfView.displayMode = .singlePage
        pdfView.displayDirection = .horizontal
        pdfView.delegate = context.coordinator
        return pdfView
    }
    
    func updateUIView(_ uiView: PDFView, context: Context) {
        uiView.document = document
        if let document = document, let page = document.page(at: currentPage - 1) {
            uiView.go(to: page)
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, PDFViewDelegate {
        var parent: PDFViewer
        
        init(_ parent: PDFViewer) {
            self.parent = parent
        }
        
        func pdfViewPageChanged(_ pdfView: PDFView) {
            if let currentPage = pdfView.currentPage, let document = pdfView.document {
                parent.currentPage = document.index(for: currentPage) + 1
                parent.totalPages = document.pageCount
            }
        }
    }
}

struct ContentView: View {
    @State private var pdfDocument: PDFDocument?
    @State private var currentPage = 1
    @State private var totalPages = 1
    @State private var isLoading = false
    @State private var scale: CGFloat = 1.0
    let pdfURL = URL(string: "https://ontheline.trincoll.edu/images/bookdown/sample-local-pdf.pdf")!
    
    var body: some View {
        ZStack {
            if let document = pdfDocument {
                PDFViewer(document: document, currentPage: $currentPage, totalPages: $totalPages)
                    .edgesIgnoringSafeArea(.all)
                    .scaleEffect(scale)
                    .gesture(MagnificationGesture()
                        .onChanged { value in
                            self.scale = value.magnitude
                        }
                    )
                
                VStack {
                    Spacer()
                    HStack {
                        Button(action: previousPage) {
                            Image(systemName: "arrow.left")
                        }
                        .disabled(currentPage == 1)
                        
                        Text("\(currentPage) / \(totalPages)")
                        
                        Button(action: nextPage) {
                            Image(systemName: "arrow.right")
                        }
                        .disabled(currentPage == totalPages)
                        
                        Spacer()
                        
                        Button(action: zoomIn) {
                            Image(systemName: "plus.magnifyingglass")
                        }
                        
                        Button(action: zoomOut) {
                            Image(systemName: "minus.magnifyingglass")
                        }
                    }
                    .padding()
                    .background(Color.black.opacity(0.6))
                    .foregroundColor(.white)
                }
            } else {
                Text("PDF not loaded")
            }
            
            if isLoading {
                ProgressView()
            }
        }
        .onAppear(perform: loadPDF)
    }
    
    private func loadPDF() {
        isLoading = true
        URLSession.shared.dataTask(with: pdfURL) { data, response, error in
            if let data = data {
                DispatchQueue.main.async {
                    self.pdfDocument = PDFDocument(data: data)
                    self.totalPages = self.pdfDocument?.pageCount ?? 0
                    self.isLoading = false
                }
            } else {
                print("Failed to load PDF: \(error?.localizedDescription ?? "Unknown error")")
                DispatchQueue.main.async {
                    self.isLoading = false
                }
            }
        }.resume()
    }
    
    private func nextPage() {
        if currentPage < totalPages {
            currentPage += 1
        }
    }
    
    private func previousPage() {
        if currentPage > 1 {
            currentPage -= 1
        }
    }
    
    private func zoomIn() {
        scale *= 1.2
    }
    
    private func zoomOut() {
        scale /= 1.2
    }
}

#Preview {
    ContentView()
}

Output:

This implementation adds the following features:

  • Page navigation buttons (previous and next)
  • Current page display
  • Zoom-in and out buttons
  • Pinch-to-zoom gesture support

More about PDF:

Leave a Reply