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: