CtrlK
BlogDocsLog inGet started
Tessl Logo

dpearson2699/swift-ios-skills

Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.

71

Quality

89%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

pdfkit-patterns.mdskills/pdfkit/references/

PDFKit Patterns

Extended patterns for PDFKit: form filling, creating PDFs programmatically, watermarks, printing, merging documents, custom page overlays, document outlines, and custom annotation drawing.

Contents

Form Filling

PDF forms use widget annotations. Each widget has a fieldName, a widgetFieldType, and a widgetStringValue.

Reading Form Fields

func extractFormFields(from document: PDFDocument) -> [(name: String, value: String)] {
    var fields: [(String, String)] = []
    for pageIndex in 0..<document.pageCount {
        guard let page = document.page(at: pageIndex) else { continue }
        for annotation in page.annotations {
            guard annotation.widgetFieldType == .text,
                  let name = annotation.fieldName else { continue }
            fields.append((name, annotation.widgetStringValue ?? ""))
        }
    }
    return fields
}

Filling Text Fields

func fillTextField(in document: PDFDocument, fieldName: String, value: String) {
    for pageIndex in 0..<document.pageCount {
        guard let page = document.page(at: pageIndex) else { continue }
        for annotation in page.annotations {
            if annotation.widgetFieldType == .text,
               annotation.fieldName == fieldName {
                annotation.widgetStringValue = value
                return
            }
        }
    }
}

Filling Checkbox Fields

func setCheckbox(in document: PDFDocument, fieldName: String, checked: Bool) {
    for pageIndex in 0..<document.pageCount {
        guard let page = document.page(at: pageIndex) else { continue }
        for annotation in page.annotations {
            if annotation.widgetFieldType == .button,
               annotation.widgetControlType == .checkBoxControl,
               annotation.fieldName == fieldName {
                annotation.buttonWidgetState = checked ? .onState : .offState
                return
            }
        }
    }
}

Creating Widget Annotations

// Text field widget
func createTextField(
    bounds: CGRect,
    fieldName: String,
    placeholder: String = ""
) -> PDFAnnotation {
    let widget = PDFAnnotation(bounds: bounds, forType: .widget, withProperties: nil)
    widget.widgetFieldType = .text
    widget.fieldName = fieldName
    widget.widgetStringValue = placeholder
    widget.font = UIFont.systemFont(ofSize: 12)
    widget.backgroundColor = UIColor.systemGray6
    return widget
}

// Checkbox widget
func createCheckbox(bounds: CGRect, fieldName: String) -> PDFAnnotation {
    let widget = PDFAnnotation(bounds: bounds, forType: .widget, withProperties: nil)
    widget.widgetFieldType = .button
    widget.widgetControlType = .checkBoxControl
    widget.fieldName = fieldName
    widget.buttonWidgetState = .offState
    return widget
}

// Radio button widget
func createRadioButton(
    bounds: CGRect,
    groupName: String,
    stateString: String
) -> PDFAnnotation {
    let widget = PDFAnnotation(bounds: bounds, forType: .widget, withProperties: nil)
    widget.widgetFieldType = .button
    widget.widgetControlType = .radioButtonControl
    widget.fieldName = groupName
    widget.buttonWidgetStateString = stateString
    return widget
}

// Choice widget (dropdown)
func createDropdown(
    bounds: CGRect,
    fieldName: String,
    options: [String]
) -> PDFAnnotation {
    let widget = PDFAnnotation(bounds: bounds, forType: .widget, withProperties: nil)
    widget.widgetFieldType = .choice
    widget.fieldName = fieldName
    widget.choices = options
    widget.isListChoice = false  // false = dropdown, true = list box
    return widget
}

Building a Simple Form

func buildForm() -> PDFDocument {
    let document = PDFDocument()
    let page = PDFPage()
    let pageBounds = CGRect(x: 0, y: 0, width: 612, height: 792) // Letter size
    page.setBounds(pageBounds, for: .mediaBox)

    // Name field
    let nameField = createTextField(
        bounds: CGRect(x: 100, y: 700, width: 200, height: 24),
        fieldName: "name",
        placeholder: ""
    )
    page.addAnnotation(nameField)

    // Agree checkbox
    let checkbox = createCheckbox(
        bounds: CGRect(x: 100, y: 660, width: 20, height: 20),
        fieldName: "agree"
    )
    page.addAnnotation(checkbox)

    document.insert(page, at: 0)
    return document
}

Creating PDFs Programmatically

From Images

func createPDFFromImages(_ images: [UIImage]) -> PDFDocument {
    let document = PDFDocument()
    for (index, image) in images.enumerated() {
        if let page = PDFPage(image: image) {
            document.insert(page, at: index)
        }
    }
    return document
}

From Images with Options

func createPDFFromImage(
    _ image: UIImage,
    mediaBox: CGRect? = nil,
    compressionQuality: CGFloat = 0.8
) -> PDFPage? {
    var options: [PDFPage.ImageInitializationOption: Any] = [
        .compressionQuality: compressionQuality
    ]
    if let box = mediaBox {
        options[.mediaBox] = NSValue(cgRect: box)
    }
    return PDFPage(image: image, options: options)
}

Using Core Graphics

For full control over PDF layout, use UIGraphicsPDFRenderer.

func createPDFWithCoreGraphics() -> Data {
    let pageRect = CGRect(x: 0, y: 0, width: 612, height: 792)
    let renderer = UIGraphicsPDFRenderer(bounds: pageRect)

    return renderer.pdfData { context in
        context.beginPage()

        // Draw title
        let title = "Document Title"
        let titleAttributes: [NSAttributedString.Key: Any] = [
            .font: UIFont.boldSystemFont(ofSize: 24),
            .foregroundColor: UIColor.black
        ]
        title.draw(at: CGPoint(x: 50, y: 50), withAttributes: titleAttributes)

        // Draw body text
        let body = "This is the body text of the PDF document."
        let bodyAttributes: [NSAttributedString.Key: Any] = [
            .font: UIFont.systemFont(ofSize: 12),
            .foregroundColor: UIColor.darkGray
        ]
        let bodyRect = CGRect(x: 50, y: 100, width: 512, height: 600)
        body.draw(in: bodyRect, withAttributes: bodyAttributes)

        // Draw a line
        context.cgContext.setStrokeColor(UIColor.gray.cgColor)
        context.cgContext.setLineWidth(1)
        context.cgContext.move(to: CGPoint(x: 50, y: 90))
        context.cgContext.addLine(to: CGPoint(x: 562, y: 90))
        context.cgContext.strokePath()
    }
}

Loading Core Graphics PDF into PDFDocument

let pdfData = createPDFWithCoreGraphics()
let document = PDFDocument(data: pdfData)

Watermarks

Add watermarks by subclassing PDFPage and overriding draw(with:to:).

Text Watermark

class WatermarkedPage: PDFPage {
    var watermarkText: String = "CONFIDENTIAL"

    override func draw(with box: PDFDisplayBox, to context: CGContext) {
        super.draw(with: box, to: context)

        UIGraphicsPushContext(context)
        context.saveGState()

        let pageBounds = bounds(for: box)
        let center = CGPoint(x: pageBounds.midX, y: pageBounds.midY)

        // Rotate around center
        context.translateBy(x: center.x, y: center.y)
        context.rotate(by: -.pi / 4)  // -45 degrees
        context.translateBy(x: -center.x, y: -center.y)

        let attributes: [NSAttributedString.Key: Any] = [
            .font: UIFont.boldSystemFont(ofSize: 72),
            .foregroundColor: UIColor.red.withAlphaComponent(0.15)
        ]

        let textSize = watermarkText.size(withAttributes: attributes)
        let textOrigin = CGPoint(
            x: center.x - textSize.width / 2,
            y: center.y - textSize.height / 2
        )
        watermarkText.draw(at: textOrigin, withAttributes: attributes)

        context.restoreGState()
        UIGraphicsPopContext()
    }
}

Applying Watermarks via Document Delegate

class WatermarkDelegate: NSObject, PDFDocumentDelegate {
    func classForPage() -> AnyClass {
        WatermarkedPage.self
    }
}

// Usage
let delegate = WatermarkDelegate()
document.delegate = delegate
pdfView.document = document

Image Watermark

class ImageWatermarkedPage: PDFPage {
    var watermarkImage: UIImage?

    override func draw(with box: PDFDisplayBox, to context: CGContext) {
        super.draw(with: box, to: context)

        guard let image = watermarkImage?.cgImage else { return }

        let pageBounds = bounds(for: box)
        let imageSize = CGSize(width: 200, height: 200)
        let imageRect = CGRect(
            x: pageBounds.midX - imageSize.width / 2,
            y: pageBounds.midY - imageSize.height / 2,
            width: imageSize.width,
            height: imageSize.height
        )

        context.saveGState()
        context.setAlpha(0.1)
        context.draw(image, in: imageRect)
        context.restoreGState()
    }
}

Merging Documents

Append All Pages

func mergeDocuments(_ documents: [PDFDocument]) -> PDFDocument {
    let merged = PDFDocument()
    var insertIndex = 0
    for document in documents {
        for pageIndex in 0..<document.pageCount {
            guard let page = document.page(at: pageIndex) else { continue }
            merged.insert(page, at: insertIndex)
            insertIndex += 1
        }
    }
    return merged
}

Extract Page Range

func extractPages(
    from document: PDFDocument,
    range: ClosedRange<Int>
) -> PDFDocument {
    let extracted = PDFDocument()
    var insertIndex = 0
    for pageIndex in range {
        guard pageIndex < document.pageCount,
              let page = document.page(at: pageIndex) else { continue }
        extracted.insert(page, at: insertIndex)
        insertIndex += 1
    }
    return extracted
}

Split Document

func splitDocument(_ document: PDFDocument, pagesPerChunk: Int) -> [PDFDocument] {
    var chunks: [PDFDocument] = []
    var chunkDoc = PDFDocument()
    var chunkIndex = 0

    for pageIndex in 0..<document.pageCount {
        guard let page = document.page(at: pageIndex) else { continue }
        chunkDoc.insert(page, at: chunkIndex)
        chunkIndex += 1

        if chunkIndex >= pagesPerChunk {
            chunks.append(chunkDoc)
            chunkDoc = PDFDocument()
            chunkIndex = 0
        }
    }

    if chunkDoc.pageCount > 0 {
        chunks.append(chunkDoc)
    }

    return chunks
}

Printing

Using UIPrintInteractionController

func printPDF(document: PDFDocument, from viewController: UIViewController) {
    guard let data = document.dataRepresentation() else { return }

    let printController = UIPrintInteractionController.shared
    let printInfo = UIPrintInfo(dictionary: nil)
    printInfo.outputType = .general
    printInfo.jobName = "PDF Document"

    printController.printInfo = printInfo
    printController.printingItem = data
    printController.present(animated: true)
}

Print a Specific Page Range

func printPageRange(
    document: PDFDocument,
    range: ClosedRange<Int>,
    from viewController: UIViewController
) {
    let subset = extractPages(from: document, range: range)
    guard let data = subset.dataRepresentation() else { return }

    let printController = UIPrintInteractionController.shared
    let printInfo = UIPrintInfo(dictionary: nil)
    printInfo.outputType = .general
    printController.printInfo = printInfo
    printController.printingItem = data
    printController.present(animated: true)
}

Document Outlines

PDFOutline represents the table of contents (bookmarks) of a PDF.

Reading Outlines

func printOutline(_ outline: PDFOutline, level: Int = 0) {
    let indent = String(repeating: "  ", count: level)
    if let label = outline.label {
        print("\(indent)\(label)")
    }
    for i in 0..<outline.numberOfChildren {
        if let child = outline.child(at: i) {
            printOutline(child, level: level + 1)
        }
    }
}

// Usage
if let root = document.outlineRoot {
    printOutline(root)
}

Creating Outlines

func buildOutline(for document: PDFDocument) {
    let root = PDFOutline()

    let chapter1 = PDFOutline()
    chapter1.label = "Chapter 1"
    if let page = document.page(at: 0) {
        chapter1.destination = PDFDestination(page: page, at: .zero)
    }
    root.insertChild(chapter1, at: 0)

    let section1_1 = PDFOutline()
    section1_1.label = "Section 1.1"
    if let page = document.page(at: 2) {
        section1_1.destination = PDFDestination(page: page, at: .zero)
    }
    chapter1.insertChild(section1_1, at: 0)

    let chapter2 = PDFOutline()
    chapter2.label = "Chapter 2"
    if let page = document.page(at: 5) {
        chapter2.destination = PDFDestination(page: page, at: .zero)
    }
    root.insertChild(chapter2, at: 1)

    document.outlineRoot = root
}

Navigating to an Outline Entry

func goToOutlineEntry(_ outline: PDFOutline, in pdfView: PDFView) {
    if let destination = outline.destination {
        pdfView.go(to: destination)
    } else if let action = outline.action {
        pdfView.perform(action)
    }
}

Custom Annotation Subclasses

Override draw(with:in:) to render custom annotation graphics.

class CircleStampAnnotation: PDFAnnotation {
    override func draw(with box: PDFDisplayBox, in context: CGContext) {
        super.draw(with: box, in: context)

        UIGraphicsPushContext(context)
        context.saveGState()

        let insetBounds = bounds.insetBy(dx: 2, dy: 2)
        let path = UIBezierPath(ovalIn: insetBounds)
        path.lineWidth = 3

        UIColor.systemRed.setStroke()
        path.stroke()

        // Draw centered text
        let text = "REVIEWED"
        let attributes: [NSAttributedString.Key: Any] = [
            .font: UIFont.boldSystemFont(ofSize: 10),
            .foregroundColor: UIColor.systemRed
        ]
        let textSize = text.size(withAttributes: attributes)
        let textOrigin = CGPoint(
            x: bounds.midX - textSize.width / 2,
            y: bounds.midY - textSize.height / 2
        )
        text.draw(at: textOrigin, withAttributes: attributes)

        context.restoreGState()
        UIGraphicsPopContext()
    }
}

Registering Custom Annotations via Delegate

class AnnotationDelegate: NSObject, PDFDocumentDelegate {
    func `class`(forAnnotationType annotationType: String) -> AnyClass {
        switch annotationType {
        case "CircleStamp":
            return CircleStampAnnotation.self
        default:
            return PDFAnnotation.self
        }
    }
}

Custom Page Subclasses

Override draw(with:to:) for page-level custom drawing like headers, footers, or decorative borders.

class HeaderFooterPage: PDFPage {
    var headerText: String = ""
    var footerText: String = ""

    override func draw(with box: PDFDisplayBox, to context: CGContext) {
        super.draw(with: box, to: context)

        UIGraphicsPushContext(context)
        context.saveGState()

        let pageBounds = bounds(for: box)
        let attributes: [NSAttributedString.Key: Any] = [
            .font: UIFont.systemFont(ofSize: 10),
            .foregroundColor: UIColor.gray
        ]

        // Header (top of page in PDF coordinates)
        if !headerText.isEmpty {
            let headerSize = headerText.size(withAttributes: attributes)
            let headerOrigin = CGPoint(
                x: pageBounds.midX - headerSize.width / 2,
                y: pageBounds.maxY - 30
            )
            headerText.draw(at: headerOrigin, withAttributes: attributes)
        }

        // Footer (bottom of page in PDF coordinates)
        if !footerText.isEmpty {
            let footerSize = footerText.size(withAttributes: attributes)
            let footerOrigin = CGPoint(
                x: pageBounds.midX - footerSize.width / 2,
                y: 20
            )
            footerText.draw(at: footerOrigin, withAttributes: attributes)
        }

        context.restoreGState()
        UIGraphicsPopContext()
    }
}

Rendering Pages to Images

Single Page

func renderPage(_ page: PDFPage, scale: CGFloat = 2.0) -> UIImage? {
    let pageBounds = page.bounds(for: .mediaBox)
    let size = CGSize(
        width: pageBounds.width * scale,
        height: pageBounds.height * scale
    )

    let renderer = UIGraphicsImageRenderer(size: size)
    return renderer.image { ctx in
        ctx.cgContext.scaleBy(x: scale, y: scale)

        // White background
        UIColor.white.setFill()
        ctx.fill(CGRect(origin: .zero, size: size))

        // PDFPage draw uses bottom-left origin; flip the context
        ctx.cgContext.translateBy(x: 0, y: pageBounds.height)
        ctx.cgContext.scaleBy(x: 1, y: -1)

        page.draw(with: .mediaBox, to: ctx.cgContext)
    }
}

Thumbnail Shortcut

The simpler thumbnail(of:for:) method handles coordinate flipping internally.

let thumbnail = page.thumbnail(of: CGSize(width: 200, height: 260), for: .mediaBox)

Page Rotation and Cropping

Rotation

// Rotate a page (must be a multiple of 90)
page.rotation = 90   // 0, 90, 180, or 270

Cropping

Set the crop box to display only a portion of the page.

let pageBounds = page.bounds(for: .mediaBox)
let cropRect = pageBounds.insetBy(dx: 50, dy: 50)
page.setBounds(cropRect, for: .cropBox)

Burning In Annotations

Write annotations permanently into the PDF content so they cannot be removed.

func burnInAnnotations(_ document: PDFDocument, to url: URL) -> Bool {
    document.write(to: url, withOptions: [
        .burnInAnnotationsOption: true
    ])
}

After burning in, annotations become part of the page content and are no longer editable or removable as separate objects.

Document Permissions

Check what operations the PDF allows.

func checkPermissions(_ document: PDFDocument) {
    let status = document.permissionsStatus  // .none, .user, .owner

    let canCopy = document.allowsCopying
    let canPrint = document.allowsPrinting
    let canComment = document.allowsCommenting
    let canFillForms = document.allowsFormFieldEntry
    let canAssemble = document.allowsDocumentAssembly
    let canModify = document.allowsDocumentChanges
    let canAccessibility = document.allowsContentAccessibility
}

Writing with Access Permissions

func saveWithRestrictions(_ document: PDFDocument, to url: URL) {
    let permissions: PDFAccessPermissions = [
        .allowsLowQualityPrinting,
        .allowsContentCopying,
        .allowsCommenting
    ]
    document.write(to: url, withOptions: [
        .ownerPasswordOption: "ownerSecret",
        .userPasswordOption: "userPass",
        .accessPermissionsOption: permissions.rawValue
    ])
}

Coordinator Pattern for PDFView

A reusable coordinator that handles delegate callbacks, notifications, and annotation hit detection for a SwiftUI-wrapped PDFView.

import SwiftUI
import PDFKit

struct ManagedPDFView: UIViewRepresentable {
    let document: PDFDocument
    @Binding var currentPageIndex: Int
    var onAnnotationTapped: ((PDFAnnotation) -> Void)?

    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        pdfView.autoScales = true
        pdfView.displayMode = .singlePageContinuous
        pdfView.document = document
        pdfView.delegate = context.coordinator
        return pdfView
    }

    func updateUIView(_ pdfView: PDFView, context: Context) {
        if pdfView.document !== document {
            pdfView.document = document
        }
        if let page = document.page(at: currentPageIndex),
           pdfView.currentPage !== page {
            pdfView.go(to: page)
        }
    }

    func makeCoordinator() -> Coordinator { Coordinator(self) }

    class Coordinator: NSObject, PDFViewDelegate {
        let parent: ManagedPDFView

        init(_ parent: ManagedPDFView) {
            self.parent = parent
            super.init()

            NotificationCenter.default.addObserver(
                self,
                selector: #selector(pageChanged),
                name: .PDFViewPageChanged,
                object: nil
            )
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(annotationHit),
                name: .PDFViewAnnotationHit,
                object: nil
            )
        }

        @objc func pageChanged(_ notification: Notification) {
            guard let pdfView = notification.object as? PDFView,
                  let page = pdfView.currentPage,
                  let doc = pdfView.document else { return }
            let index = doc.index(for: page)
            if parent.currentPageIndex != index {
                parent.currentPageIndex = index
            }
        }

        @objc func annotationHit(_ notification: Notification) {
            guard let annotation = notification.userInfo?["PDFAnnotationHit"] as? PDFAnnotation
            else { return }
            parent.onAnnotationTapped?(annotation)
        }

        func pdfViewWillClick(onLink sender: PDFView, with url: URL) {
            UIApplication.shared.open(url)
        }
    }
}

skills

CHANGELOG.md

README.md

tile.json