Skip to content

PDF-SDK 2.4.1 is out.See what’s new

PDF-SDK
Skip to Content
Docs

iOS, iPadOS & Mac Catalyst

A complete editor UI you present with one view: PDFEditorView in SwiftUI or PDFEditorViewController in UIKit. Give it a PDF file, get the edited bytes back.

Every type and option: iOS, iPadOS & Mac Catalyst API reference.

1. Add the Swift package

In Xcode choose File ▸ Add Package Dependencies…, enter the package URL that comes with your license, and add the PDFEditorUI product to your app target:

ProductForMinimum
PDFEditorUIiPhone, iPad, Mac Catalyst (SwiftUI + UIKit)iOS 15 · Catalyst 15

Or in Package.swift:

dependencies: [
    .package(url: "<your PDF-SDK package URL>", from: "2.4.1"),
],
targets: [
    .target(name: "MyApp", dependencies: [
        // `package:` is the URL's last path component — "pdf-sdk" for …/pdf-sdk.git
        .product(name: "PDFEditorUI", package: "pdf-sdk"),       // iOS / iPadOS / Catalyst
    ]),
]

On the Mac, the same product runs in your app built for Mac Catalyst: in the app target's General ▸ Supported Destinations, add Mac (Mac Catalyst). There is no separate Mac product to add.

The engine is a prebuilt binary inside the package (device, simulator and Mac Catalyst slices; Apple silicon and Intel). There is nothing else to install and no build script to run.

2. Set your license key

Once, at launch, before the first editor opens. The key must list your app's bundle identifier.

import SwiftUI
import PDFEditorUI

@main
struct MyApp: App {
    init() {
        PDFEditorSDK.setLicenseKey("PE1.eyJ2Ijox…")
    }
    var body: some Scene { WindowGroup { ContentView() } }
}

In a UIKit app, call it in application(_:didFinishLaunchingWithOptions:). App extensions have their own bundle identifier and call it in their own entry point. Without a valid key the editor shows "PDF-SDK is not licensed for this app" with the reason instead of the document — see License keys.

3. Present the editor

SwiftUI

import SwiftUI
import PDFEditorUI

struct DocumentScreen: View {
    let pdfURL: URL                      // a file in your app's container
    @State private var editing = false

    var body: some View {
        Button("Edit") { editing = true }
            .fullScreenCover(isPresented: $editing) {
                PDFEditorView(url: pdfURL) { editedPDF in        // Data
                    try? editedPDF.write(to: pdfURL, options: .atomic)
                    editing = false
                } onCancel: {
                    editing = false
                }
            }
    }
}

UIKit

import PDFEditorUI

// in your view controller
func edit(_ pdfURL: URL) {
    let editor = PDFEditorViewController(
        url: pdfURL,
        onSave: { [weak self] editedPDF in
            try? editedPDF.write(to: pdfURL, options: .atomic)
            self?.dismiss(animated: true)
        },
        onCancel: { [weak self] in self?.dismiss(animated: true) })
    editor.modalPresentationStyle = .fullScreen
    present(editor, animated: true)
}

Open a file in your app's container. The editor reads url and never writes it: the edited PDF comes to onSave, and you store it. A file picked with .fileImporter is security-scoped, and the editor can't read it as it is — copy it into your container (between startAccessingSecurityScopedResource() and stop…), then open the copy.

Options

ArgumentTypeWhat it does
urlURLthe PDF to open — a file in your app's container
initialModePDFEditorMode?iOS: open in .contentEditor (default), .viewer, .form (not on Mac Catalyst), .annotate, .fillSign, .insert or .draw
configurationPDFEditorConfigurationiOS: modes, tools, menu items, colour scheme, tool styles, zoom — see Customize
initialToolPDFEditorTool?iOS: pre-arm a tool: .addText, .image, .link, .insertPage, .multiSelect, .highlight, .underline, .strikeout, .squiggly, .signature, .stamp, .note, .draw, .shapeRectangle, .shapeEllipse, .shapeLine, .shapeArrow, and the form fields .formTextField, .formCheckBox, .formRadioButton, .formListBox, .formDropdown
onSave(Data) -> Voidthe edited PDF, when the user taps Done
onCancel() -> Voidthe user closed without saving
onSharePDF, onPrint, onFlattenAndShare((Data) -> Void)?iOS: intercept the ⋮ menu's export actions instead of the system sheet
onDigitallySigned((Data) -> Void)?iOS: the signed PDF after an e-signature
onUnsavedChangesChanged((Bool) -> Void)?iOS: tracks unsaved edits — use it on Mac Catalyst, where a window can close without asking

Customize the editor

Pass a PDFEditorConfiguration as configuration: to choose which modes and tools the editor offers, hide ⋮-menu items, pin light or dark, and set the tools' first-use styles. Leave it out and you get the editor as it ships. These options narrow or restyle what the editor already does; the sections below also rename things and add your own buttons and fonts. Out-of-range values are clamped.

var config = PDFEditorConfiguration()
config.modes = [.viewer, .annotate, .fillSign]        // listed in this order
config.hiddenTools = [.stamp, .link]
config.hiddenMenuItems = [.print, .resetToOriginal]
config.colorScheme = .dark
config.toolDefaults.highlight = .init(color: .systemGreen, opacity: 0.4)
config.zoomRange = 1...2

PDFEditorView(url: pdfURL, configuration: config) { edited in … } onCancel: { … }
OptionDefaultNotes
modesevery mode (no Form Editor on Mac Catalyst)Your order; a mode whose tools are all hidden is dropped (not .viewer). With one mode the header shows its name, no picker. initialMode falls back to the first listed.
hiddenToolsnoneHidden in every mode that carries the tool. A hidden initialTool is not armed.
hiddenMenuItemsnone.search, .managePages, .sharePDF, .print, .flattenAndShare, .resetForm, .resetToOriginal. With all hidden, the ⋮ button goes.
colorScheme.system.light or .dark for the editor's presentation.
toolDefaultswarm yellow, black penFirst-use styles for highlight, underline, strikeout, squiggly and ink. A user's own later choice is remembered and wins.
zoomRange0.5...3Can narrow, never widen; always includes 1 (the fitted page). 1...1 disables pinch-zoom.
haptics, showsUsageHintstrueTurn off haptic feedback, or the one-time "drag across text…" hints.
signaturePlacement.bottomRightWhere the e-Sign tab's Placement picker starts: .bottomLeft, .topRight, .topLeft or .invisible. The user can still pick another before signing.

Names, icons, order and your own buttons

config.modeTitles = [.fillSign: "Sign"]
config.toolTitles = [.signature: "Sign here"]
config.toolIcons  = [.highlight: UIImage(named: "marker")!]
config.customTools[.fillSign] = [
    PDFEditorCustomAction(id: "approve", title: "Approve", image: UIImage(systemName: "checkmark.seal")) { ctx in
        if let pdf = ctx.currentPDF() { approve(pdf, page: ctx.pageIndex) }
    }
]
config.toolOrder[.fillSign] = [.custom("approve"), .tool(.addText)]
config.customMenuItems = [PDFEditorCustomAction(id: "crm", title: "Send to CRM") { ctx in
    if let pdf = ctx.currentPDF() { upload(pdf) }
}]
config.menuOrder = [.custom("crm")]

Names: toolTitles, modeTitles, menuItemTitles. Icons, drawn as templates so they tint like the built-in glyphs: toolIcons, modeIcons, menuItemIcons, headerIcons. Your own buttons: customTools[mode] and customMenuItems. A tap commits the edit in progress and calls your handler, which can read the document with ctx.currentPDF() (nil if the export failed). Order: toolOrder[mode] and menuOrder put the listed items first, and everything else follows in its usual order. A tool can be reordered within its mode, not moved to another.

Fonts

config.interfaceFontFamily = "Avenir Next"
config.fonts = [PDFEditorFont(url: Bundle.main.url(forResource: "Brand-Regular", withExtension: "ttf")!)]
config.fontsTitle = "Brand"
config.allowsFontImport = false

interfaceFontFamily sets the editor's own text (toolbars, header, sheets) in a family your app can load; sizes stay the editor's, and SF Symbols, system menus and the document's text keep their own fonts. fonts lists your fonts first in the text font picker, under fontsTitle; each must be a TrueType/OpenType file, and one that isn't is skipped. showsDocumentFonts, showsDeviceFonts and allowsFontImport hide the picker's other sections. newTextFont sets the font of every text box the user places; its size and colour still follow the text around it.

Selection

config.selectionColor = UIColor(named: "Brand")
config.handleShape = .circle
config.handleScale = 1.25
config.selectionBorderWidth = 2
config.rotateIcon = UIImage(systemName: "arrow.triangle.2.circlepath")

selectionColor colours the selection box, its handles, the rotation knob, the text caret, the text-selection band and the page-alignment guide (a dynamic colour follows light/dark). handleFillColor and handleShape (.roundedSquare, .circle, .square) style the handles; handleScale (0.75–1.5) sizes them, and a mouse's reach follows while a finger's stays the same. selectionBorderWidth is 1–3 pt. rotateIcon is drawn in the knob as a template, in the selection colour. searchHighlightColor and searchCurrentHighlightColor colour the search highlights; their translucency stays the editor's, so the text under them reads.

The configuration applies on iPhone, iPad and Mac Catalyst alike.

Edit documents from code

Everything a user can do in the editor can be done from code, with no UI: read every layer of a page and its properties, replace text in any script (the layer keeps its font, size, colour and position, and the system's fonts fill in letters its font lacks), restyle, move, resize, rotate and restack layers, add text, images and shapes, mark text up, fill and author forms, edit metadata and pages, search, render a page, and save.

A layer is addressed by its page and an id that keeps naming it while other layers change. Geometry is in page points from the page's top-left corner. Each change is one undo step.

let doc = try PDFDocumentEditor(data: pdfData)
for block in doc.textBlocks(page: 0) {
    try doc.setText(block, translate(block.text ?? ""), shrinkToFit: true)
}
let edited = try doc.save()

Pass onDocumentReady to PDFEditorView or PDFEditorViewController to get the document the editor is showing: changes appear on screen at once and the user can undo them. Failures throw PDFDocumentError; .notFound means the layer is gone, so read the layers again.

Localize the editor

Every word the editor shows has a stable key, the same on every platform, with English as its built-in text. Give text for the keys you want translated; the rest stay English. The editor checks a dictionary first, then a provider function, then the app's own PDFEditor.strings and Localizable.strings in its current language, then English. Right-to-left languages mirror the editor; the page navigator stays left to right, like the pages it turns.

var config = PDFEditorConfiguration()
config.localization = PDFEditorLocalization(
    language: "hi",
    strings: ["cancel": "रद्द करें", "done": "हो गया"]
)

Placeholders are positional (Page {1} of {2}), so a translation can reorder them. A count has two keys: <key>_one and <key>. Names you set per tool, mode or menu item win over translations.

Digital signatures

Fill & Sign ▸ Signature ▸ e-Sign creates a self-signed Digital ID on the device and applies a PKCS#7 detached signature. Receive the signed bytes through onDigitallySigned — a separate channel from onSave:

PDFEditorView(url: pdfURL) { edited in
    try? edited.write(to: pdfURL, options: .atomic); editing = false
} onCancel: {
    editing = false
} onDigitallySigned: { signedPDF in
    try? signedPDF.write(to: pdfURL, options: .atomic)
    editing = false
}

Mac (Mac Catalyst)

On the Mac, the editor is the same PDFEditorView / PDFEditorViewController, with the same configuration and callbacks, running in your Mac Catalyst app. It adds desktop behaviour: selection on pointer-down, ⌘/⇧-click to add to a selection, ⌘= / ⌘− / ⌘0 and ⌘-scroll to zoom, drag to pan a zoomed page, and the Mac colour panel. Two differences from iPhone and iPad:

  • There is no Form Editor mode on Mac Catalyst.
  • A Mac window can close without asking, so track onUnsavedChangesChanged and save or confirm before it closes.

License status

let license = PDFEditorSDK.license
if !license.isLicensed {
    print(license.status, license.reason)   // e.g. .wrongApp — names the bundle id it checked
}

PDFEditorSDK.version is the SDK version — log it next to your own build number so crash reports identify both.

Troubleshooting

SymptomFix
"PDF-SDK is not licensed for this app"Read the reason on screen. A debug or extension bundle id differs from the release one — ask for it to be added, or for a com.acme.* wildcard.
A picked file opens empty or fails to openIt's security-scoped — copy it into your container first, then open the copy.
The file isn't updated after DoneThe editor never writes url; write the bytes from onSave yourself, with do/catch rather than try? so a failure shows.
"minimum deployment target of iOS 15.0"Raise your app target to iOS 15.
Chinese text shows boxesThe document names a CJK font it does not embed; the package bundles a fallback — check the app is not stripping bundle resources.