Custom Annotation Provider in Swift for iOS
Use a custom annotation provider. Get additional resources by visiting our guide on saving PDF annotations to external storage in iOS.
//// Copyright © 2021-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
class CustomAnnotationProviderExample: Example {
override init() { super.init() title = "Custom Annotation Provider" contentDescription = "Shows how to use a custom annotation provider" category = .annotationProviders }
override func invoke(with delegate: ExampleRunnerDelegate?) -> UIViewController? { let document = AssetLoader.document(for: .annualReport)
document.didCreateDocumentProviderBlock = { documentProvider in documentProvider.annotationManager.annotationProviders = [ // Assign a CustomAnnotationProvider instance as one of the // annotation providers for the document. CustomAnnotationProvider(documentProvider: documentProvider) ] }
let controller = PDFViewController(document: document) return controller }}
private class CustomAnnotationProvider: PDFContainerAnnotationProvider { /// Timer used to used to update annotation color and notify the delegates. private var timer: Timer?
// MARK: - Lifecycle override init(documentProvider: PDFDocumentProvider) { super.init(documentProvider: documentProvider)
// Add timer in a way so it works while we're dragging pages (NSRunLoopCommonModes). timer = Timer(timeInterval: 1.0, target: self, selector: #selector(timerFired(_:)), userInfo: nil, repeats: true)
// The document provider generation can happen on any thread, make sure we register on the main runloop. if let timer { RunLoop.main.add(timer, forMode: .common) } }
// MARK: - AnnotationProvider
/// Backing store for the `AnnotationProvider` protocol's `providerDelegate` property. private var providerDelegateBackingStore: AnnotationProviderChangeNotifier?
override var providerDelegate: AnnotationProviderChangeNotifier? { get { providerDelegateBackingStore } set { guard newValue?.isEqual(providerDelegate) == false else { return }
providerDelegateBackingStore = newValue
// Nil out timer to allow object to deallocate itself. if newValue == nil { timer?.invalidate() timer = nil } } }
override func annotationsForPage(at pageIndex: PageIndex) -> [Annotation]? { // It's important that this method is: // - Fast // - Thread safe // - Caches annotations — Doesn't always create new objects. if let cachedAnnotations = performRead({ super.annotationsForPage(at: pageIndex) }) { return cachedAnnotations }
// We have nothing cached yet, so populate the cache. return performWriteAndWait { // Since we left the critical region, another thread may have raced here before. // So if we omitted this second check, we would occasionally duplicate annotations! if let cachedAnnotations = super.annotationsForPage(at: pageIndex) { return cachedAnnotations }
// Create a new note annotation and add it to the backing annotation dictionary storage. let documentProvider = providerDelegate?.parentDocumentProvider let noteAnnotation = NoteAnnotation() noteAnnotation.contents = "Annotation from the custom annotationProvider for page index \(pageIndex)."
let pageInfo = documentProvider?.document?.pageInfoForPage(at: pageIndex) let pageHeight = pageInfo?.size.height ?? 500.0 // Using a fallback page height of 500.
// Place it on top left (PDF coordinate space starts from bottom left). // See https://www.nutrient.io/guides/ios/faq/coordinate-spaces/ for more info. noteAnnotation.boundingBox = CGRect(x: 100.0, y: pageHeight - 100.0, width: 32.0, height: 32.0)
// Set page as the last step, and register the annotation with the backing store. noteAnnotation.pageIndex = pageIndex noteAnnotation.isEditable = false let confirmedAdditions = super.add([noteAnnotation], options: [.suppressNotifications: true])
// Adding annotations to the backing store typically sets the needs save flag. But if you get the // annotations added here from an external file, nothing really changed. So we should make sure to // clear this flag if we have no dirty annotations. if dirtyAnnotations?.isEmpty ?? true { clearNeedsSaveFlag() }
return confirmedAdditions } }
// MARK: - Helper
// Change annotation color and notify the delegate that we have updates. @objc func timerFired(_ timer: Timer?) { let color = RandomColor()
// Accessing the annotations backing store for a synchronized access to avoid // multithreading issues. let cachedAnnotations = performRead { annotationCache as! [Int: [Annotation]] } cachedAnnotations.forEach { _, annotationsOnPage in annotationsOnPage.forEach { $0.color = color } providerDelegate?.update(annotationsOnPage, animated: true) } }}
// MARK: - Private
// Helper to generate a random color.private func RandomColor() -> UIColor? { let hue = CGFloat.random(in: 0...1.0) let saturation = CGFloat.random(in: 0.5...1.0) // 0.5 to 1.0, away from white let brightness = CGFloat.random(in: 0.5...1.0) // 0.5 to 1.0, away from black return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)}
This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.