Hide or reveal an area in a PDF using Swift for iOS

Allow users to select areas to hide and reveal on a page using a custom square annotation.


//
// Copyright © 2020-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
class HideRevealAreaExample: Example {
nonisolated static let hideAreaKey = "hideArea"
nonisolated static let revealAreaKey = "revealArea"
override init() {
super.init()
title = "Hide/Reveal Area"
contentDescription = "Allow users to select areas to hide/reveal on a page"
category = .annotations
priority = 9
}
override func invoke(with delegate: ExampleRunnerDelegate) -> UIViewController {
let document = AssetLoader.document(for: .foodAndDrinks)
// We use a subclassed annotation manager to use a custom annotation view for reveal areas.
document.overrideClass(AnnotationManager.self, with: CustomAnnotationManager.self)
// We use a subclassed square annotation to display hide and reveal area as overlay
document.overrideClass(SquareAnnotation.self, with: CustomSquareAnnotation.self)
let controller = HideRevealAreaPDFViewController(document: document, configuration: PDFConfiguration {
$0.pageTransition = .scrollPerSpread
$0.pageMode = .single
$0.createAnnotationMenuGroups = []
$0.isTextSelectionEnabled = false
$0.allowToolbarTitleChange = false
$0.isAutosaveEnabled = false
$0.thumbnailBarMode = .none
// We use a subclassed resizable view to deliver resizing/moving events to the tracking view.
$0.overrideClass(ResizableView.self, with: CustomResizableView.self)
})
return controller
}
}
private class HideRevealAreaPDFViewController: PDFViewController {
let revealAreaButton: UIBarButtonItem
let hideAreaButton: UIBarButtonItem
override init(document: Document?, configuration: PDFConfiguration?) {
revealAreaButton = UIBarButtonItem()
hideAreaButton = UIBarButtonItem()
super.init(document: document, configuration: configuration)
delegate = self
documentViewController?.delegate = self
// Add reveal area and hide area buttons to the navigation bar and make sure their title is always updated
revealAreaButton.target = self
revealAreaButton.action = #selector(toggleRevealArea(_:))
hideAreaButton.target = self
hideAreaButton.action = #selector(toggleHideArea(_:))
navigationItem.setRightBarButtonItems([hideAreaButton, revealAreaButton], for: .document, animated: false)
updateButtonTitles()
NotificationCenter.default.addObserver(self, selector: #selector(updateButtonTitles), name: .PSPDFAnnotationsAdded, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(updateButtonTitles), name: .PSPDFAnnotationsRemoved, object: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func toggleRevealArea(_ sender: UIBarButtonItem) {
// Check if there is an existing revealed area. If yes, delete it, if not, add a new one.
if let revealArea = customSquare(onPageIndex: pageIndex, customDataString: HideRevealAreaExample.revealAreaKey) {
document?.remove(annotations: [revealArea])
} else {
addRevealArea()
}
}
@objc func toggleHideArea(_ sender: UIBarButtonItem) {
// Check if there is an existing hidden area. If yes, delete it, if not, add a new one.
if let hideArea = customSquare(onPageIndex: pageIndex, customDataString: HideRevealAreaExample.hideAreaKey) {
document?.remove(annotations: [hideArea])
} else {
addHideArea()
}
}
func customSquare(onPageIndex pageIndex: PageIndex, customDataString: String) -> Annotation? {
// Reveal and hide areas use customData to be detectable.
// We check here if a square annotation on a given page contains the customData value that is passed in.
if let squares = document?.annotationsForPage(at: pageIndex, type: .square) {
let customAnnotation = squares.first { annotation -> Bool in
if let customDataValue = annotation.customData?[customDataString] as? Bool, customDataValue == true {
return true
}
return false
}
return customAnnotation
}
return nil
}
func addRevealArea() {
// Create, and add a new reveal area to the current page, and select it.
let pageIndex = self.pageIndex
let revealArea = CustomSquareAnnotation()
revealArea.isRevealArea = true
revealArea.boundingBox = CGRect(x: 51, y: 462, width: 309, height: 168)
revealArea.color = UIColor.clear
// Use a color other than clear, to make tapping the transparent area select the annotation.
revealArea.fillColor = UIColor.white.withAlphaComponent(0.0)
revealArea.lineWidth = 0
revealArea.pageIndex = pageIndex
document?.add(annotations: [revealArea])
pageViewForPage(at: pageIndex)?.selectedAnnotations = [revealArea]
}
func addHideArea() {
// Create, and add a new hide area to the current page, and select it.
let pageIndex = self.pageIndex
let hideAreaAnnotation = CustomSquareAnnotation()
hideAreaAnnotation.isHideArea = true
hideAreaAnnotation.boundingBox = CGRect(x: 360, y: 80.5, width: 201, height: 552)
hideAreaAnnotation.color = UIColor.clear
hideAreaAnnotation.fillColor = UIColor.black
hideAreaAnnotation.lineWidth = 0
hideAreaAnnotation.pageIndex = pageIndex
document?.add(annotations: [hideAreaAnnotation])
pageViewForPage(at: pageIndex)?.selectedAnnotations = [hideAreaAnnotation]
}
@objc func updateButtonTitles() {
self.revealAreaButton.title = "Reveal Area"
self.hideAreaButton.title = "Hide Area"
if customSquare(onPageIndex: pageIndex, customDataString: HideRevealAreaExample.revealAreaKey) != nil {
self.revealAreaButton.title = "Reset Reveal Area"
}
if customSquare(onPageIndex: pageIndex, customDataString: HideRevealAreaExample.hideAreaKey) != nil {
self.hideAreaButton.title = "Reset Hide Area"
}
}
}
extension HideRevealAreaPDFViewController: PDFViewControllerDelegate {
func pdfViewController(_ sender: PDFViewController, menuForAnnotations annotations: [Annotation], onPageView pageView: PDFPageView, appearance: EditMenuAppearance, suggestedMenu: UIMenu) -> UIMenu {
// Keep only the Delete action.
suggestedMenu
.keep(actions: [.PSPDFKit.delete])
}
func pdfViewController(_ sender: PDFViewController, menuForCreatingAnnotationAt point: CGPoint, onPageView pageView: PDFPageView, appearance: EditMenuAppearance, suggestedMenu: UIMenu) -> UIMenu {
// Disable the annotation creation menu entirely.
UIMenu(children: [])
}
}
extension HideRevealAreaPDFViewController: PDFDocumentViewControllerDelegate {
func documentViewController(_ documentViewController: PDFDocumentViewController, didChangeSpreadIndex oldSpreadIndex: Int) {
updateButtonTitles()
}
}
private class CustomSquareAnnotation: SquareAnnotation {
override class var supportsSecureCoding: Bool {
return true
}
override var isOverlay: Bool {
get {
// Always display reveal and hide areas as overlay,
// which means that they are always rendered using their annotation view.
if isRevealArea || isHideArea {
return true
}
return super.isOverlay
}
set { super.isOverlay = newValue }
}
}
private extension Annotation {
var isRevealArea: Bool {
// Use customData to mark an annotation as a reveal area.
get { return customData?[HideRevealAreaExample.revealAreaKey] as? Bool ?? false }
set { customData = [HideRevealAreaExample.revealAreaKey: newValue] }
}
var isHideArea: Bool {
// Use customData to mark an annotation as a hide area.
get { return customData?[HideRevealAreaExample.hideAreaKey] as? Bool ?? false }
set { customData = [HideRevealAreaExample.hideAreaKey: newValue] }
}
}
private class CustomAnnotationManager: AnnotationManager {
override func annotationViewClass(for annotation: Annotation) -> AnyClass? {
// Use a custom annotation view subclass for reveal areas.
if annotation.isRevealArea {
return RevealAreaView.self
}
return super.annotationViewClass(for: annotation)
}
}
// Annotation view that displays a transparent area for the actual annotation bounding box,
// and a black area for the rest of the page.
private class RevealAreaView: AnnotationView {
let backgroundView: UIView
let fillLayer: CAShapeLayer
override init(frame: CGRect) {
// Add a background view with a shape layer to get the transparent area behavior we want.
backgroundView = UIView()
fillLayer = CAShapeLayer()
fillLayer.fillRule = CAShapeLayerFillRule.evenOdd
fillLayer.fillColor = UIColor.black.cgColor
backgroundView.layer.addSublayer(fillLayer)
isEditing = false
super.init(frame: frame)
self.addSubview(backgroundView)
self.sendSubviewToBack(backgroundView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var isEditing: Bool {
didSet {
// Make the page content shine through when we are editing (resizing or moving).
fillLayer.opacity = isEditing ? 0.8 : 1
}
}
override var pageView: PDFPageView? {
didSet {
if let pageView {
updateBackgroundFrame(withBounds: pageView.bounds)
}
}
}
override func didChangePageBounds(_ bounds: CGRect) {
updateBackgroundFrame(withBounds: bounds)
}
override var frame: CGRect {
didSet {
if let pageView {
updateBackgroundFrame(withBounds: pageView.bounds)
// Whenever the annotation view's frame change, we want to update the path for the shape layer.
// We use a bezier path consiting of the page view bounds and the current annotation view's frame
// while using the evenOdd fill rule, to get the expected behavior with the page content being covered in black
// while the actual annotation bounding box is still transparent, with the page content being visible.
let pagePath = UIBezierPath(rect: pageView.bounds)
let annotationPath = UIBezierPath(rect: frame)
pagePath.append(annotationPath)
pagePath.usesEvenOddFillRule = true
fillLayer.path = pagePath.cgPath
}
}
}
func updateBackgroundFrame(withBounds bounds: CGRect) {
// Make sure the background view is always positioned correctly, covering the whole page view.
var backgroundFrame = bounds
backgroundFrame.origin = CGPoint(x: -self.frame.origin.x, y: -self.frame.origin.y)
self.backgroundView.frame = backgroundFrame
}
}
private class CustomResizableView: ResizableView, ResizableViewDelegate {
override init(frame: CGRect) {
super.init(frame: frame)
self.delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Let the annotation view know when the reveal area is resized or moved.
func resizableViewDidBeginEditing(_ resizableView: ResizableView) {
guard let revealAreaView = self.trackedViews?.first(where: { $0 is RevealAreaView }) as? RevealAreaView else { return }
revealAreaView.isEditing = true
}
func resizableViewDidEndEditing(_ resizableView: ResizableView, didChangeFrame: Bool) {
guard let revealAreaView = self.trackedViews?.first(where: { $0 is RevealAreaView }) as? RevealAreaView else { return }
revealAreaView.isEditing = false
}
}

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