Generate PDF from SwiftUI view using PDFKit – iOS

Using ImageRenderer, we can create PDFs from a SwiftUI view. Here is a quick example:

import SwiftUI
import PDFKit

struct ContentView: View {
    
    var body: some View {
        VStack{
            ViewForPDF()
            
            Button("Generate PDF from View") {
                generatePDF()
            }
        }
    }
    
    @MainActor func generatePDF() {
        let renderer = ImageRenderer(content: ViewForPDF())
        
        let url = FileManager.default.temporaryDirectory.appendingPathComponent("output.pdf")
        
        renderer.render { size, context in
            var mediaBox = CGRect(origin: .zero, size: size)
            guard let pdf = CGContext(url as CFURL, mediaBox: &mediaBox, nil) else { return }
            
            pdf.beginPDFPage(nil)
            context(pdf)
            pdf.endPDFPage()
            pdf.closePDF()
        }
        
        // Now you can use the PDF at 'url'
        print("PDF saved at: \(url.path)")
    }
}


struct ViewForPDF: View {
    
    var body: some View {
        VStack{
            Text("This is a PDF!")
                .font(.title)
            Image(systemName: "globe")
                .font(.system(size: 200))
            Rectangle().fill(.blue)
                .frame(width: 200, height: 200)
            
        }
        .padding()
    }
}

If you run the project and click on the Generate PDF from View button, it will generate a PDF for you. You will able to see the file URL on the console. If you copy and paste the URL in the Browser, you will able to see the pdf.

/Users/name/Library/Developer/CoreSimulator/Devices/..../data/Containers/Data/Application/.../tmp/output.pdf

The output will be:

You can generate PDFs from complex SwiftUI view too. Here is an example:

import SwiftUI
import PDFKit

struct ContentView: View {
    
    var body: some View {
        VStack{
            ViewForPDF()
            
            Button("Generate PDF from View") {
                generatePDF()
            }
        }
    }
    
    @MainActor func generatePDF() {
        let renderer = ImageRenderer(content: ViewForPDF().frame(width:612, height: 792 )) // A4 Size
        let url = FileManager.default.temporaryDirectory.appendingPathComponent("output.pdf")
        
        renderer.render { size, context in
            var mediaBox = CGRect(origin: .zero, size: size)
            guard let pdf = CGContext(url as CFURL, mediaBox: &mediaBox, nil) else { return }
            
            pdf.beginPDFPage(nil)
            context(pdf)
            pdf.endPDFPage()
            pdf.closePDF()
        }
        
        // Now you can use the PDF at 'url'
        print("PDF saved at: \(url.path)")
    }
}


struct ViewForPDF: View {
    
    var body: some View {
        VStack {
            VStack{
                HStack {
                    Image(systemName: "person.fill")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 100, height: 100)
                        .padding()
                        .background(.teal)
                        .clipShape(Circle())
                    
                    VStack(alignment: .leading){
                        Text("Your Name")
                            .font(.headline)
                            .foregroundColor(.white)
                            .padding(.top, 0)
                        
                        Text("Your Title")
                            .font(.subheadline)
                            .foregroundColor(.white)
                            .padding(.top, 0)
                    }
                    Spacer()
                }
                
            }
            .padding()
            .frame(maxWidth: .infinity)
            .background(Color(.systemGray4))
            .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous))
            
            
            
            VStack(alignment: .leading, spacing: 10) {
                Text("**Email:** [email protected]")
                Text("**Location:** Planet Earth, Andromeda Galaxy, Observable universe!")
                Text("**Phone:** +123-456-876")
            }
        }
        .padding()
    }
}

#Preview{
    ContentView()
}

You can generate PDFs from any data too. Here is another example:

import SwiftUI
import PDFKit

struct ContentView: View {
    let data: [(name: String, number: String, address: String)] = [
        ("John Doe", "123-456-7890", "123 Main St"),
        ("Jane Smith", "098-765-4321", "456 Elm St"),
        // Add more data as needed
    ]
    
    var body: some View {
        Button("Generate PDF") {
            generatePDF()
        }
    }
    
    @MainActor func generatePDF() {
        let renderer = ImageRenderer(content: DataListView(data: data))
        
        let url = FileManager.default.temporaryDirectory.appendingPathComponent("output.pdf")
        
        renderer.render { size, context in
            var mediaBox = CGRect(origin: .zero, size: size)
            guard let pdf = CGContext(url as CFURL, mediaBox: &mediaBox, nil) else { return }
            
            pdf.beginPDFPage(nil)
            context(pdf)
            pdf.endPDFPage()
            pdf.closePDF()
        }
        
        // Now you can use the PDF at 'url'
        print("PDF saved at: \(url.path)")
    }
}

struct DataListView: View {
    let data: [(name: String, number: String, address: String)]
    
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Data List").font(.title).padding(.bottom)
            ForEach(data, id: \.name) { item in
                VStack(alignment: .leading) {
                    Text("Name: \(item.name)")
                    Text("Number: \(item.number)")
                    Text("Address: \(item.address)")
                }
                .padding(.bottom, 5)
            }
        }
        .padding()
    }
}

Output:

If you increase the data, it will increase the page length. It does not add a new page in PDF. So we can modify our code so that it can add a new page based on data size. Here is the modified code:

import SwiftUI
import PDFKit

struct ContentView: View {
    let data = [
         Person(name: "John Doe", number: "123-456-7890", address: "123 Main St"),
         Person(name: "Jane Smith", number: "987-654-3210", address: "456 Elm St"),
         Person(name: "Sam Johnson", number: "555-123-4567", address: "789 Oak St"),
         Person(name: "Alice Brown", number: "123-987-6543", address: "101 Maple St"),
         Person(name: "Bob White", number: "456-321-7890", address: "202 Pine St"),
         Person(name: "John Doe", number: "123-456-7890", address: "123 Main St"),
         Person(name: "Jane Smith", number: "987-654-3210", address: "456 Elm St"),
         Person(name: "Sam Johnson", number: "555-123-4567", address: "789 Oak St"),
         Person(name: "Alice Brown", number: "123-987-6543", address: "101 Maple St"),
         Person(name: "Bob White", number: "456-321-7890", address: "202 Pine St"),
     ]
    var body: some View {
        VStack{
      
            Button("Generate PDF") {
                generatePDF()
            }
        }
    }
    
    @MainActor func generatePDF() {
         let pageSize = CGSize(width: 612, height: 792) // A4 size
        let renderer = ImageRenderer(content: DataListView(data: data))
        
        let url = FileManager.default.temporaryDirectory.appendingPathComponent("output.pdf")
        
        var currentPage = 1
        let maxPages = Int(ceil(Double(data.count) / 6.0)) // Assume 6 items per page
        
        renderer.render { size, context in
            var mediaBox = CGRect(origin: .zero, size: pageSize)

            guard let pdf = CGContext(url as CFURL, mediaBox: &mediaBox, nil) else { return }
                     
            
            while currentPage <= maxPages {
                pdf.beginPDFPage(nil)
                
                // Render the current page
                let currentPageData = Array(data[(currentPage - 1) * 6..<min(currentPage * 6, data.count)])
                
                let pageView = DataListView(data: currentPageData).frame(width: pageSize.width, height: pageSize.height)
                
                
                let pageRenderer = ImageRenderer(content: pageView)
                pageRenderer.render { _, pageContext in
                    pageContext(pdf)
                }
                
                pdf.endPDFPage()
                currentPage += 1
            }
            
            pdf.closePDF()
        }
        
        // Now you can use the PDF at 'url'
        print("PDF saved at: \(url.path)")
    }
}

struct DataListView: View {
    let data: [Person]
    
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Data List").font(.title).padding(.bottom)
            ForEach(data) { item in
                VStack(alignment: .leading) {
                    Text("Name: \(item.name)")
                    Text("Number: \(item.number)")
                    Text("Address: \(item.address)")
                }
                .padding(.bottom, 5)
            }
            Spacer()
        }
        .padding()
    }
}


struct Person: Identifiable {
    let id = UUID()
    let name: String
    let number: String
    let address: String
}

Now if you generate a PDF, you will see it will create a new page if required.

For now, we assume that on each page, we can fit 6 items. But we can modify the code to change items on each page too:

    @MainActor func generatePDF() {
         let pageSize = CGSize(width: 612, height: 792) // A4 size
        let renderer = ImageRenderer(content: DataListView(data: data))
        
        let url = FileManager.default.temporaryDirectory.appendingPathComponent("output.pdf")
        
        var currentPage = 1
        let itemsPerPage = 4  // Assume 4 items per page
        
        let maxPages = Int(ceil(Double(data.count) / Double(itemsPerPage)))
        
        renderer.render { size, context in
            var mediaBox = CGRect(origin: .zero, size: pageSize)

            guard let pdf = CGContext(url as CFURL, mediaBox: &mediaBox, nil) else { return }
                     
            
            while currentPage <= maxPages {
                pdf.beginPDFPage(nil)
                
                // Render the current page
                let currentPageData = Array(data[(currentPage - 1) * itemsPerPage..<min(currentPage * itemsPerPage, data.count)])
                
                let pageView = DataListView(data: currentPageData).frame(width: pageSize.width, height: pageSize.height)
                
                
                let pageRenderer = ImageRenderer(content: pageView)
                pageRenderer.render { _, pageContext in
                    pageContext(pdf)
                }
                
                pdf.endPDFPage()
                currentPage += 1
            }
            
            pdf.closePDF()
        }
        
        // Now you can use the PDF at 'url'
        print("PDF saved at: \(url.path)")
    }

Change itemsPerPage number to fit items as you need. The output will be like this:

If you want to generate PDFs programmatically without generating SwiftUI view, you can follow this tutorial: Create a PDF in SwiftUI using PDFKit from Dynamic Data

Leave a Reply