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:
| Product | For | Minimum |
|---|---|---|
PDFEditorUI | iPhone, 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
| Argument | Type | What it does |
|---|---|---|
url | URL | the PDF to open — a file in your app's container |
initialMode | PDFEditorMode? | iOS: open in .contentEditor (default), .viewer, .form (not on Mac Catalyst), .annotate, .fillSign, .insert or .draw |
configuration | PDFEditorConfiguration | iOS: modes, tools, menu items, colour scheme, tool styles, zoom — see Customize |
initialTool | PDFEditorTool? | 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) -> Void | the edited PDF, when the user taps Done |
onCancel | () -> Void | the 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: { … }| Option | Default | Notes |
|---|---|---|
modes | every 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. |
hiddenTools | none | Hidden in every mode that carries the tool. A hidden initialTool is not armed. |
hiddenMenuItems | none | .search, .managePages, .sharePDF, .print, .flattenAndShare, .resetForm, .resetToOriginal. With all hidden, the ⋮ button goes. |
colorScheme | .system | .light or .dark for the editor's presentation. |
toolDefaults | warm yellow, black pen | First-use styles for highlight, underline, strikeout, squiggly and ink. A user's own later choice is remembered and wins. |
zoomRange | 0.5...3 | Can narrow, never widen; always includes 1 (the fitted page). 1...1 disables pinch-zoom. |
haptics, showsUsageHints | true | Turn off haptic feedback, or the one-time "drag across text…" hints. |
signaturePlacement | .bottomRight | Where 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 = falseinterfaceFontFamily 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
onUnsavedChangesChangedand 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
| Symptom | Fix |
|---|---|
| "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 open | It's security-scoped — copy it into your container first, then open the copy. |
| The file isn't updated after Done | The 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 boxes | The document names a CJK font it does not embed; the package bundles a fallback — check the app is not stripping bundle resources. |