```html SwiftUI: How to Implement PDF Generation (iOS 17+, 2026)

How to Implement PDF Generation in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: PDFKit / UIGraphicsPDFRenderer Updated: May 11, 2026
TL;DR

Use UIGraphicsPDFRenderer to draw text, images, and shapes into one or more pages, then turn the result into Data you can share or save. Wrap the share step in a ShareLink or UIActivityViewController to hand the file off to Mail, Files, or AirDrop.

import SwiftUI

struct MinimalPDFView: View {
    @State private var pdfData: Data?

    var body: some View {
        Button("Generate PDF") {
            pdfData = makePDF()
        }
        .sheet(item: $pdfData) { data in
            ShareSheet(items: [data])
        }
    }

    func makePDF() -> Data {
        let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: 612, height: 792))
        return renderer.pdfData { ctx in
            ctx.beginPage()
            let attrs: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 24)]
            "Hello, PDF!".draw(at: CGPoint(x: 72, y: 72), withAttributes: attrs)
        }
    }
}

Full implementation

The example below builds a structured invoice-style PDF with a header, a table of line items, a running total, and a footer — across multiple pages if the content overflows. A PDFGenerator struct keeps all drawing logic out of the view layer, keeping things testable. The SwiftUI view owns only the trigger button and the share sheet.

import SwiftUI
import PDFKit

// MARK: - Data model

struct LineItem: Identifiable {
    let id = UUID()
    var description: String
    var quantity: Int
    var unitPrice: Double
    var total: Double { Double(quantity) * unitPrice }
}

// MARK: - PDF generator

struct PDFGenerator {
    static let pageWidth: CGFloat  = 612   // US Letter
    static let pageHeight: CGFloat = 792
    static let margin: CGFloat     = 72

    static func generate(title: String, items: [LineItem]) -> Data {
        let bounds = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
        let renderer = UIGraphicsPDFRenderer(bounds: bounds)

        return renderer.pdfData { ctx in
            ctx.beginPage()
            var cursor: CGFloat = margin

            // — Header —
            cursor = drawHeader(title: title, cursor: cursor)

            // — Table —
            for item in items {
                if cursor > pageHeight - margin * 2 {
                    ctx.beginPage()
                    cursor = margin
                }
                cursor = drawLineItem(item, cursor: cursor)
            }

            // — Total —
            let grandTotal = items.reduce(0) { $0 + $1.total }
            cursor = drawTotal(grandTotal, cursor: cursor + 8)

            // — Footer —
            drawFooter()
        }
    }

    // MARK: Drawing helpers

    @discardableResult
    private static func drawHeader(title: String, cursor: CGFloat) -> CGFloat {
        let titleAttrs: [NSAttributedString.Key: Any] = [
            .font: UIFont.boldSystemFont(ofSize: 28),
            .foregroundColor: UIColor.black
        ]
        title.draw(at: CGPoint(x: margin, y: cursor), withAttributes: titleAttrs)

        let line = UIBezierPath()
        line.move(to:    CGPoint(x: margin, y: cursor + 40))
        line.addLine(to: CGPoint(x: pageWidth - margin, y: cursor + 40))
        UIColor.systemGray3.setStroke()
        line.lineWidth = 1
        line.stroke()

        return cursor + 56
    }

    @discardableResult
    private static func drawLineItem(_ item: LineItem, cursor: CGFloat) -> CGFloat {
        let bodyAttrs: [NSAttributedString.Key: Any] = [
            .font: UIFont.systemFont(ofSize: 13),
            .foregroundColor: UIColor.darkText
        ]
        let priceAttrs: [NSAttributedString.Key: Any] = [
            .font: UIFont.monospacedDigitSystemFont(ofSize: 13, weight: .regular),
            .foregroundColor: UIColor.darkText
        ]

        let descRect  = CGRect(x: margin, y: cursor, width: 280, height: 20)
        let qtyRect   = CGRect(x: 370, y: cursor, width: 60, height: 20)
        let totalRect = CGRect(x: 460, y: cursor, width: pageWidth - margin - 460, height: 20)

        item.description.draw(in: descRect, withAttributes: bodyAttrs)
        "× \(item.quantity)".draw(in: qtyRect, withAttributes: bodyAttrs)
        String(format: "$%.2f", item.total).draw(in: totalRect, withAttributes: priceAttrs)

        return cursor + 24
    }

    @discardableResult
    private static func drawTotal(_ total: Double, cursor: CGFloat) -> CGFloat {
        let line = UIBezierPath()
        line.move(to:    CGPoint(x: 420, y: cursor))
        line.addLine(to: CGPoint(x: pageWidth - margin, y: cursor))
        UIColor.systemGray3.setStroke()
        line.stroke()

        let totalAttrs: [NSAttributedString.Key: Any] = [
            .font: UIFont.boldSystemFont(ofSize: 15),
            .foregroundColor: UIColor.black
        ]
        let priceAttrs: [NSAttributedString.Key: Any] = [
            .font: UIFont.monospacedDigitSystemFont(ofSize: 15, weight: .bold),
            .foregroundColor: UIColor.black
        ]
        "Total".draw(at: CGPoint(x: 420, y: cursor + 8), withAttributes: totalAttrs)
        String(format: "$%.2f", total).draw(at: CGPoint(x: 490, y: cursor + 8), withAttributes: priceAttrs)
        return cursor + 32
    }

    private static func drawFooter() {
        let footerAttrs: [NSAttributedString.Key: Any] = [
            .font: UIFont.systemFont(ofSize: 10),
            .foregroundColor: UIColor.systemGray
        ]
        let text = "Generated by Soarias · soarias.com"
        text.draw(at: CGPoint(x: margin, y: pageHeight - margin + 8), withAttributes: footerAttrs)
    }
}

// MARK: - SwiftUI view

struct PDFGeneratorView: View {
    private let items: [LineItem] = [
        LineItem(description: "SwiftUI Consulting",  quantity: 3, unitPrice: 250),
        LineItem(description: "Code Review Session", quantity: 1, unitPrice: 180),
        LineItem(description: "App Icon Design",     quantity: 1, unitPrice: 120),
    ]

    @State private var generatedPDF: Data?
    @State private var isGenerating = false

    var body: some View {
        NavigationStack {
            List(items) { item in
                HStack {
                    VStack(alignment: .leading) {
                        Text(item.description).font(.headline)
                        Text("Qty: \(item.quantity)").font(.caption).foregroundStyle(.secondary)
                    }
                    Spacer()
                    Text(String(format: "$%.2f", item.total))
                        .monospacedDigit()
                        .foregroundStyle(.primary)
                }
                .padding(.vertical, 4)
            }
            .navigationTitle("Invoice")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button {
                        generate()
                    } label: {
                        if isGenerating {
                            ProgressView().controlSize(.small)
                        } else {
                            Label("Export PDF", systemImage: "square.and.arrow.up")
                        }
                    }
                    .disabled(isGenerating)
                }
            }
            .sheet(item: $generatedPDF) { data in
                ShareSheet(activityItems: [data])
                    .presentationDetents([.medium, .large])
            }
        }
    }

    private func generate() {
        isGenerating = true
        Task.detached(priority: .userInitiated) {
            let data = PDFGenerator.generate(title: "Invoice #0042", items: items)
            await MainActor.run {
                generatedPDF = data
                isGenerating = false
            }
        }
    }
}

// MARK: - ShareSheet bridge

struct ShareSheet: UIViewControllerRepresentable {
    let activityItems: [Any]
    func makeUIViewController(context: Context) -> UIActivityViewController {
        UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
    }
    func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}

// MARK: - Data + Identifiable convenience

extension Data: @retroactive Identifiable {
    public var id: Int { hashValue }
}

#Preview {
    PDFGeneratorView()
}

How it works

  1. UIGraphicsPDFRenderer — Instantiated with a CGRect matching US Letter (612 × 792 pts). Calling renderer.pdfData { ctx in … } returns ready-to-use Data without any temp files. Each ctx.beginPage() call appends a new page.
  2. Cursor-based layout — The cursor variable tracks the current Y position. Each drawing helper returns the updated cursor after it finishes, enabling automatic overflow detection: when cursor > pageHeight - margin * 2, a new page begins and the cursor resets to the top margin.
  3. Text drawing with NSAttributedString attributesString.draw(at:withAttributes:) and String.draw(in:withAttributes:) place styled text. UIFont.monospacedDigitSystemFont keeps currency figures aligned without a custom font.
  4. Background thread generationTask.detached(priority: .userInitiated) keeps the main thread free during rendering. The result is published back on MainActor to safely update @State.
  5. ShareSheet — A thin UIViewControllerRepresentable wrapping UIActivityViewController presents the system share sheet. Passing raw Data lets iOS automatically offer Mail, Files, AirDrop, and Print — no extra configuration needed.

Variants

Embed an image (logo, chart, photo)

// Inside a beginPage block
if let logo = UIImage(named: "AppLogo") {
    let logoRect = CGRect(x: margin, y: cursor, width: 120, height: 40)
    logo.draw(in: logoRect)
    cursor += 50
}

// For a SwiftUI view snapshot (iOS 17+):
let renderer2 = ImageRenderer(content: ChartView(data: salesData))
renderer2.scale = 2          // retina
if let uiImage = renderer2.uiImage {
    let chartRect = CGRect(x: margin, y: cursor, width: 468, height: 240)
    uiImage.draw(in: chartRect)
    cursor += 250
}

Save to the Documents directory instead of sharing

If your app needs to keep PDFs locally (e.g. for offline viewing with PDFView), write the Data directly to disk:

func savePDF(_ data: Data, named filename: String) throws -> URL {
    let docs = try FileManager.default.url(
        for: .documentDirectory,
        in: .userDomainMask,
        appropriateFor: nil,
        create: true
    )
    let url = docs.appending(path: filename)
    try data.write(to: url, options: .atomic)
    return url
}

// Display inline with PDFKit
import PDFKit

struct InlinePDFView: UIViewRepresentable {
    let data: Data
    func makeUIView(context: Context) -> PDFView {
        let view = PDFView()
        view.autoScales  = true
        view.document    = PDFDocument(data: data)
        return view
    }
    func updateUIView(_ uiView: PDFView, context: Context) {}
}

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement pdf generation in SwiftUI for iOS 17+.
Use PDFKit/UIGraphicsPDFRenderer.
Make it accessible (VoiceOver labels).
Add a #Preview with realistic sample data.

Drop this prompt into the Soarias Build phase after your screens have been scaffolded — it slots into the document-export step of your ship cycle without touching unrelated views.

Related

FAQ

Does this work on iOS 16?

UIGraphicsPDFRenderer has been available since iOS 10, so the core rendering code runs on iOS 16. However, the #Preview macro and ImageRenderer (used to snapshot SwiftUI views) both require iOS 16+. The Data: @retroactive Identifiable conformance and the .presentationDetents modifier for the sheet require iOS 16+. If you need iOS 15 support, replace the sheet(item:) pattern with a @State var showShare = false bool.

Can I render a SwiftUI view directly into a PDF without UIKit drawing calls?

Yes — use ImageRenderer (iOS 16+) to capture any SwiftUI view as a UIImage, then draw that image into a UIGraphicsPDFRenderer page. This is the cleanest path for chart or card thumbnails. For full-document layouts with reflowable text and precise page breaks, the manual cursor approach shown above gives you more control.

What's the UIKit equivalent?

In UIKit you use the same UIGraphicsPDFRenderer API — it's not SwiftUI-specific. Alternatively, PDFDocument and PDFPage from PDFKit let you build documents from existing PDF assets or annotate pages programmatically. For UIKit-only apps targeting older OS versions, the lower-level UIGraphicsBeginPDFContextToData / UIGraphicsEndPDFContext pair (deprecated in style but still functional) achieves the same result.

Last reviewed: 2026-05-11 by the Soarias team.

```