Auto Save PDF Annotation Changes in Swift for iOS
Add an alert after detecting annotation writes to define a new save location for the modified document. Get additional resources by visiting our Other Constants API guide.
//// Copyright © 2019-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 PSPDFKitimport PSPDFKitUI
// This class will ask the user as soon as the first annotation has been added/modified where the annotation should be saved, and optionally copies the file to a new location.
class AnnotationSaveAsForAnnotationEditingExample: Example { override init() { super.init() title = "Save as... for annotation editing" contentDescription = "Adds an alert after detecting annotation writes to define a new save location." category = .annotations priority = 100 }
override func invoke(with delegate: ExampleRunnerDelegate) -> UIViewController? { let document = AssetLoader.writableDocument(for: .annualReport, overrideIfExists: false) return SaveAsPDFViewController(document: document) }}
// MARK: PDFViewController
private class SaveAsPDFViewController: PDFViewController { var hasUserBeenAskedAboutSaveLocation = false private var observer: Any?
override func commonInit(with document: Document?, configuration: PDFConfiguration) { super.commonInit(with: document, configuration: configuration)
navigationItem.rightBarButtonItems = [thumbnailsButtonItem, annotationButtonItem]
let closeButton = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(closeButtonTapped(_:))) navigationItem.leftBarButtonItems = [closeButton]
NotificationCenter.default.addObserver(self, selector: #selector(annotationChangedNotification), name: .PSPDFAnnotationChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(annotationAddedOrRemovedNotification), name: .PSPDFAnnotationsAdded, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(annotationAddedOrRemovedNotification), name: .PSPDFAnnotationsRemoved, object: nil) }
deinit { // Clear document cache, so we don't get annotation-artefacts when loading the doc again. SDK.shared.cache.remove(for: document) }
// MARK: Private @objc private func closeButtonTapped(_ sender: UIBarButtonItem) { annotationStateManager.state = nil // Commit any annotations navigationController?.popViewController(animated: true) }
@objc private func annotationChangedNotification(_ notification: Notification) { guard let notificationObject = notification.object as? Annotation else { return } processChangeForAnnotation(annotation: notificationObject) }
@objc private func annotationAddedOrRemovedNotification(_ notification: NSNotification) { guard let notificationObject = notification.object as? [Annotation] else { return } for annotation in notificationObject { processChangeForAnnotation(annotation: annotation) } }
private func processChangeForAnnotation(annotation: Annotation) { if annotation.document == document { self.askUserAboutSaveLocationIfNeeded() } }
// Mark - Document Copying Logic
// This code assumes that the PDF location itself is writeable, and will fail for documents in the bundle folder. private func askUserAboutSaveLocationIfNeeded() { // Make sure the alert gets displayed only once per session if hasUserBeenAskedAboutSaveLocation { return } hasUserBeenAskedAboutSaveLocation = true
let alertController = UIAlertController(title: nil, message: "Would you like to save annotations into the current file, or create a copy to save the annotation changes?", preferredStyle: .alert) alertController.addAction(UIAlertAction(title: "Save to This File", style: .destructive)) alertController.addAction(UIAlertAction(title: "Save as Copy", style: .default, handler: { _ in self.replaceDocumentWithCopy() })) present(alertController, animated: true, completion: nil) }
private func replaceDocumentWithCopy() { guard let documentURL = document?.fileURL else { return }
let newURL = adjustedFileURLForDocumentURL(documentURL)
do { try FileManager.default.copyItem(at: documentURL, to: newURL) // Since the annotation has already been edited, we copy the file *before* it will be saved // then save the current state and switch out the documents. } catch { print("Failed to copy file to \(newURL.path): \(error.localizedDescription)") return }
do { try document?.save() } catch { print("Failed to save document: \(error.localizedDescription)") }
let tmpURL = newURL.appendingPathExtension("temp") do { try FileManager.default.moveItem(at: documentURL, to: tmpURL) } catch { print("Failed to move file: \(error.localizedDescription)") return }
do { try FileManager.default.moveItem(at: newURL, to: documentURL) } catch { print("Failed to move file: \(error.localizedDescription)") return }
do { try FileManager.default.moveItem(at: tmpURL, to: newURL) } catch { print("Failed to move file: \(error.localizedDescription)") return }
// Finally update the fileURL, this will clear the current document cache. let newDocument = Document(url: newURL) newDocument.title = document?.title // Preserve the title
// Preserve annotation selection guard var pageView: PDFPageView = pageViewForPage(at: pageIndex) else { return } guard let selectedAnnotations = pageView.selectedAnnotations else { return }
document = newDocument
// Restore selection pageView = pageViewForPage(at: pageIndex)! var newSelectedAnnotations: [Annotation] = [] for annotation in newDocument.annotationsForPage(at: pageIndex, type: .all) { for selectedAnnotation in selectedAnnotations where annotation.name == selectedAnnotation.name { newSelectedAnnotations.append(annotation) } }
// To re-show the popover, we need to wait until the alert view disappears. DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3, execute: { pageView.select(annotations: newSelectedAnnotations, presentMenu: true, animated: true) }) }
/// Appends `_annotated` to the file basename. Also appends an incrementing integer if a file with a matching name already exists. /// /// For example, `Document.pdf` would change to `Document_annotated.pdf`. /// If `Document_annotated.pdf` already exists then `Document_annotated1.pdf` would be generated instead. Then `Document_annotated2.pdf` etc. private func adjustedFileURLForDocumentURL(_ documentURL: URL) -> URL { var appendFileCount = 0 var newPath: NSString
repeat { newPath = documentURL.path as NSString let appendSuffix = "_annotated\(appendFileCount == 0 ? "" : "\(appendFileCount)").pdf" if newPath.lowercased.hasSuffix(".pdf") { newPath = newPath.replacingOccurrences(of: ".pdf", with: appendSuffix, options: .caseInsensitive, range: NSRange(location: newPath.length - 4, length: 4)) as NSString } else { newPath = newPath.appending(appendSuffix) as NSString } appendFileCount += 1 } while FileManager.default.fileExists(atPath: newPath as String)
return NSURL.fileURL(withPath: newPath as String) }}
This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.