Add Text Annotation to PDF in Swift for iOS

Add a FreeText annotation and programmatically insert text at the caret position. Get additional resources by visiting our guide on programmatically creating PDF annotations in iOS.


//
// Copyright © 2017-2025 PSPDFKit GmbH. All rights reserved.
//
// The Nutrient sample applications are licensed with a modified BSD license.
// Please see License for details. This notice may not be removed from this file.
//
import PSPDFKit
import PSPDFKitUI
private protocol InsertTextViewControllerDelegate: NSObjectProtocol {
func insertTextViewController(_ controller: InsertTextViewController, didSelectRowAt index: Int)
}
private class InsertTextViewController: BaseTableViewController {
weak var delegate: InsertTextViewControllerDelegate?
let cellReuseIdentifier = "InsertTextViewControllerCell"
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier)
}
// MARK: - UITableViewDataSource
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath)
cell.textLabel!.text = "Insert Text #\(indexPath.row)"
return cell
}
// MARK: - UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.insertTextViewController(self, didSelectRowAt: indexPath.row)
}
}
class AddTextToFreeTextAnnotationAtCaretPositionExample: Example {
var pdfController: PDFViewController?
override init() {
super.init()
title = "Add FreeText annotation and insert text at caret position"
category = .annotations
priority = 100
}
override func invoke(with delegate: ExampleRunnerDelegate) -> UIViewController? {
let document = AssetLoader.temporaryDocument(with: "Example Document")
// Add the annotation
let freeTextAnnotation = FreeTextAnnotation()
freeTextAnnotation.color = .red
freeTextAnnotation.contents = "This is a Free Text Annotation"
freeTextAnnotation.fontSize = 20
// Make the width large to ensure the text fits on one line. This will get trimmed afterwards by the call to `sizeToFit`.
freeTextAnnotation.boundingBox = CGRect(x: 200, y: 200, width: 1000000, height: 200)
let targetPage: PageIndex = 0
freeTextAnnotation.pageIndex = targetPage
freeTextAnnotation.sizeToFit()
document.add(annotations: [freeTextAnnotation])
let controller = PDFViewController(document: document) {
$0.overrideClass(FreeTextAccessoryView.self, with: AddTextFreeTextAccessoryView.self)
}
self.pdfController = controller
// Automate selection and entering edit mode
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
// Select annotation and get the view
guard let pageView = controller.pageViewForPage(at: controller.pageIndex) else {
return
}
pageView.selectedAnnotations = [freeTextAnnotation]
guard let freeTextView = pageView.annotationView(for: freeTextAnnotation) as! FreeTextAnnotationView? else {
return
}
// Begin editing and move caret somewhere to the front.
freeTextView.beginEditing()
freeTextView.textView?.selectedRange = NSRange(location: 10, length: 0)
}
return controller
}
}
private class AddTextFreeTextAccessoryView: FreeTextAccessoryView, InsertTextViewControllerDelegate {
lazy var insertTextButton: ToolbarButton = {
let button = ToolbarButton()
button.length = 50
button.accessibilityLabel = "Insert Text"
button.setTitle("Insert", for: .normal)
button.addTarget(self, action: #selector(self.insertTextTapped), for: .touchUpInside)
return button
}()
@objc func insertTextTapped(sender: FreeTextAccessoryView) {
// Second tap should dismiss the controller.
if dismissInsertTextViewController(animated: true) {
return
}
// Present controller in a way that it's still a popover on iPhone.
let controller = InsertTextViewController()
controller.title = "Example Insert Text Controller"
controller.delegate = self
controller.modalPresentationStyle = .popover
let options: [PresentationOption: Any] = [.popoverArrowDirections: UIPopoverArrowDirection.down.rawValue, .nonAdaptive: true, .inNavigationController: true]
presentationContext?.actionDelegate.present(controller, options: options, animated: true, sender: sender)
}
@discardableResult func dismissInsertTextViewController(animated: Bool) -> Bool {
return presentationContext!.actionDelegate.dismissViewController(of: InsertTextViewController.self, animated: animated)
}
// Width changes should dismiss your popover, so ensure to add your hook here.
override func dismissPresentedViewControllers(animated: Bool) {
super.dismissPresentedViewControllers(animated: animated)
dismissInsertTextViewController(animated: animated)
}
// Adds our custom button.
override func buttons(forWidth width: CGFloat) -> [ToolbarButton] {
var buttons = super.buttons(forWidth: width)
// Insert button before "Clear".
let insertionIndex = buttons.firstIndex(of: clearButton) ?? buttons.count - 1
buttons.insert(insertTextButton, at: insertionIndex)
return buttons
}
func insertTextViewController(_ controller: InsertTextViewController, didSelectRowAt index: Int) {
// First dismiss the `InsertTextViewController`.
controller.dismiss(animated: true)
// Get current page view. It not be expected for this to fail, but potentially the view hierarchy changed underneath us.
guard let pdfController = presentationContext?.pdfController, let pageView = pdfController.pageViewForPage(at: pdfController.pageIndex) else {
return
}
// Find the first free text annotation that is selected.
// Nothing to do if no annotation is selected.
guard let freeTextAnnotation = pageView.selectedAnnotations.first(where: { $0 is FreeTextAnnotation }) as! FreeTextAnnotation? else {
return
}
// Get the view of the annotation.
let freeTextView = pageView.annotationView(for: freeTextAnnotation) as! FreeTextAnnotationView
// Get the text view and its selected text range (caret position).
// Do nothing if the text view is not in the editing mode or has no caret position (both unexpected).
guard let textView = freeTextView.textView, let selectedTextRange = textView.selectedTextRange else {
return
}
// Update text at the selected range.
let text = "--NEW TEXT AT CARET POSITION (text index #\(index))--"
textView.replace(selectedTextRange, withText: text)
}
}

This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.