Customizing Menus on iOS
PSPDFKit uses a modern UIMenu
-based menu system for context-sensitive menus when you select annotations or long press on empty space. This guide explains how to work with them and customize them.
Annotation Selection Menu
When you select one or more annotations by tapping or right-clicking them on the page, PSPDFKit will present a menu. This section describes how you can present and customize it yourself in your project.
Presenting the Annotation Selection Menu
Use select(annotations:
and set true
for the presentMenu
parameter to programmatically present the menu for selected annotations:
pageView.select(annotations: [annotation], presentMenu: true, animated: true)
Customizing the Annotation Selection Menu
Implement the pdfViewController(_:
delegate method to customize the annotation selection menu directly:
func pdfViewController(_ sender: PDFViewController, menuForAnnotations annotations: [Annotation], onPageView pageView: PDFPageView, appearance: EditMenuAppearance, suggestedMenu: UIMenu) -> UIMenu { // Return the customized `suggestedMenu`. }
To insert a custom menu element into the annotation selection menu, append or prepend it to the children of the suggestedMenu
. The following example demonstrates how to add a custom Lock or Unlock action:
func pdfViewController(_ sender: PDFViewController, menuForAnnotations annotations: [Annotation], onPageView pageView: PDFPageView, appearance: EditMenuAppearance, suggestedMenu: UIMenu) -> UIMenu { // Only customize the menu for a single annotation. guard annotations.count == 1, let annotation = annotations.first else { return suggestedMenu } // Only add the custom item when the menu appears as a context menu. guard appearance == .contextMenu else { return suggestedMenu } // Prepend either the Unlock or Lock action. if annotation.isLocked { let unlockAction = UIAction(title: "Unlock", image: UIImage(systemName: "lock.open")) { _ in annotation.flags.remove(.locked) } return suggestedMenu.replacingChildren([unlockAction] + suggestedMenu.children) } else { let lockAction = UIAction(title: "Lock", image: UIImage(systemName: "lock")) { _ in annotation.flags.insert(.locked) } return suggestedMenu.replacingChildren([lockAction] + suggestedMenu.children) } }
Returning a UIMenu
with no children will prevent the annotation selection menu from being presented. See the Common Customization Techniques section to learn more about all the different ways you can customize the suggested UIMenu
object.
![]()
We don’t recommend ignoring the suggested menu or returning an empty menu. Doing so may break important functionality such as copying, modifying, and deleting annotations. Certain actions are only possible through the annotation selection menu.
Customizing Choices in the Style Menu
To customize choices available in the Style menu for selected annotations, you don’t need to customize the menu directly. Instead, use annotationMenuConfiguration
and set the custom closure for one of the following properties:
The following example demonstrates how to customize the available color choices for free text annotations but leave the default choices for other annotations:
let configuration = PDFConfiguration { $0.annotationMenuConfiguration = AnnotationMenuConfiguration { $0.colorChoices = { property, annotation, pageView, defaultChoices in if property == .color, annotation is FreeTextAnnotation { return [.systemRed, .systemGreen] } else { return defaultChoices } } } }
To learn more about configuring the PDFViewController
using the PDFConfiguration
object, check out our Configure PDF View Controllers guide.
Annotation Creation Menu
When you long press or right-click on empty space on a page, PSPDFKit will present the menu that includes the Paste action and tools for creating different types of annotations. This section describes how you can present and customize it yourself in your project.
Presenting the Annotation Creation Menu
Use the tryToShowAnnotationMenu(at:
method to programmatically present the annotation selection menu at a given location:
viewController.interactions.tryToShowAnnotationMenu(at: point, in: coordinateSpace)
To learn more about working with the user interaction components, check out our Customize User Interactions guide.
Customizing the Annotation Creation Menu
Implement the pdfViewController(_:
delegate method to customize the annotation creation menu directly:
func pdfViewController(_ sender: PDFViewController, menuForCreatingAnnotationAt point: CGPoint, onPageView pageView: PDFPageView, appearance: EditMenuAppearance, suggestedMenu: UIMenu) -> UIMenu { // Return the customized `suggestedMenu`. }
Returning a UIMenu
with no children will prevent the annotation creation menu from being presented. However, to disable the annotation creation menu altogether, it’s recommended to set the isCreateAnnotationMenuEnabled
configuration property to false
.
See the Common Customization Techniques section to learn more about all the different ways you can customize the suggested UIMenu
object.
Customizing the Tools in the Annotation Creation Menu
To customize which tools are available in the annotation creation menu, you don’t need to customize the menu directly. Instead, use the createAnnotationMenuGroups
configuration property.
The following example demonstrates how to limit the tools to include only arrow, ellipse, distance measurement, and rectangular measurement annotations:
let configuration = PDFConfiguration { $0.createAnnotationMenuGroups = [ .init(items: [ .init( type: .line, variant: .lineArrow, configurationBlock: AnnotationToolConfiguration.ToolItem.lineConfigurationBlock() ) ]), .init(items: [ .init( type: .line, variant: .distanceMeasurement, configurationBlock: AnnotationToolConfiguration.ToolItem.measurementConfigurationBlock() ), .init( type: .square, variant: .rectangularAreaMeasurement, configurationBlock: AnnotationToolConfiguration.ToolItem.measurementConfigurationBlock() ) ]) ] }
To learn more about configuring the PDFViewController
using the PDFConfiguration
object, check out our Configure PDF View Controllers guide.
Check out the API reference for Annotation.Kind
and Annotation.ToolVariantID
to see the list of all supported tools and variants. To learn more about measurement annotations specifically, check out our Measure Distance and Area in a PDF guide.
Common Customization Techniques
This section describes a variety of customization use cases that apply to both annotation selection and annotation creation menus.
![]()
Can’t find how to customize the annotation selection or annotation selection menu for your specific use case? Please reach out to us and we’ll be happy to help!
Filtering the Suggested Menu Elements
Implement one of the UIMenu
-based delegate methods and modify the suggestedMenu
parameter to exclude certain default actions or submenus. Keep in mind that you need to search the entire menu tree, and not just the immediate children of the suggestedMenu
.
The following example demonstrates how to limit the default actions in the annotation selection menu to just Copy and Delete:
func pdfViewController(_ sender: PDFViewController, menuForAnnotations annotations: [Annotation], onPageView pageView: PDFPageView, appearance: EditMenuAppearance, suggestedMenu: UIMenu) -> UIMenu { suggestedMenu.filterActions { $0 == .PSPDFKit.copy || $0 == .PSPDFKit.delete } }
Check out the API reference for PSPDFKit’s UIAction.Identifier
and UIMenu.Identifier
namespaces to see the list of all available identifiers you can use to filter out actions and submenus from the suggestedMenu
.
The filterAction
function used in the example above isn’t part of the public API, but you can implement one in your project:
extension UIMenu { func filterActions(_ predicate: (UIAction.Identifier) -> Bool) -> UIMenu { replacingChildren(children.compactMap { element in if let action = element as? UIAction { if predicate(action.identifier) { return action } else { return nil } } else if let menu = element as? UIMenu { // Filter children of submenus recursively. return menu.filterActions(predicate) } else { return element } }) } }
Inserting Custom Actions
Implement one of the UIMenu
-based delegate methods, and modify the suggestedMenu
parameter to include a custom menu element. The following example demonstrates how to insert a Select All Annotations action into the annotation creation menu:
func pdfViewController(_ sender: PDFViewController, menuForCreatingAnnotationAt point: CGPoint, onPageView pageView: PDFPageView, appearance: EditMenuAppearance, suggestedMenu: UIMenu) -> UIMenu { // Only include the action if there are any annotations on the page. guard let annotations = pageView.document?.annotations(at: pageView.pageIndex), !annotations.isEmpty else { return suggestedMenu } // Prepend the action to the `suggestedMenu`. let selectAllAction = UIAction(title: "Select All Annotations", image: UIImage(systemName: "circle.rectangle.dashed")) { _ in pageView.select(annotations: annotations, presentMenu: true, animated: true) } return suggestedMenu.replacingChildren([selectAllAction] + suggestedMenu.children) }
You can use UIAction
to insert closure-based actions, UICommand
to insert responder chain actions, and UIMenu
to insert submenus. On iOS 16 and later, UIDeferredMenuElement
is also supported.
Inserting Submenus
Implement one of the UIMenu
-based delegate methods, and modify the suggestedMenu
parameter to include a custom submenu. PSPDFKit will take care of presenting it properly.
Customizing Based on Menu Appearance
Implement one of the UIMenu
-based delegate methods, and use the appearance
parameter to conditionally customize the menu based on whether it appears as a .horizontalBar
or a .contextMenu
.
The following example demonstrates how to conditionally prepend a Custom action to the annotation creation menu when it appears as a contextual menu, or if there’s enough space in a horizontal bar:
func pdfViewController(_ sender: PDFViewController, menuForCreatingAnnotationAt point: CGPoint, onPageView pageView: PDFPageView, appearance: EditMenuAppearance, suggestedMenu: UIMenu) -> UIMenu { // Only include the action if there's space. guard appearance == .contextMenu || pageView.traitCollecton.horizontalSizeClass == .regular else { return suggestedMenu } // Prepend the action to the `suggestedMenu`. let customAction = UIAction(title: "Custom") { _ in print("Hello from custom action!") } return suggestedMenu.replacingChildren([customAction] + suggestedMenu.children) }
Displaying Menu Elements as Images
Menu elements such as UIAction
, UICommand
, and UIMenu
will be displayed as images if they have empty titles. You can conditionally make your custom actions and submenus appear as images when a menu appears as a horizontal bar.
![]()
Always set the
accessibilityLabel
for menu element images that don’t have titles. This will ensure they’re properly discoverable with VoiceOver.
The following example demonstrates how to append a More submenu to the annotation selection menu and conditionally display it as an image:
func pdfViewController(_ sender: PDFViewController, menuForAnnotations annotations: [Annotation], onPageView pageView: PDFPageView, appearance: EditMenuAppearance, suggestedMenu: UIMenu) -> UIMenu { let customAction = UIAction(title: "Custom") { _ in print("Hello from custom action!") } // Add the accessibility label to the image. let ellipsisImage = { let image = UIImage(systemName: "ellipsis")! image.accessibilityLabel = "More" return image }() let moreSubmenu = UIMenu( // Skip the title only in `.horizontalBar` appearance. title: appearance == .horizontalBar ? "" : "More", image: ellipsisImage, children: [customAction] ) return suggestedMenu.replacingChildren(suggestedMenu.children + [moreSubmenu]) }
The More submenu from the example above will be displayed as an image when the annotation selection appears as a horizontal bar, but it’ll retain its title when the menu appears as a contextual menu.
Text and Image Selection Menus
![]()
The text and image selection menu uses the legacy menu system, so none of the customization techniques described above apply. The text and image selection menu will adopt the modern menu system in a future version of PSPDFKit for iOS.
PSPDFKit uses UIMenuController
for context-sensitive menus when you select text or images. There are two delegates in PDFViewControllerDelegate
to customize this behavior:
-
pdfViewController(_:shouldShow:atSuggestedTargetRect:forSelectedText:in:on:)
-
pdfViewController(_:shouldShow:atSuggestedTargetRect:forSelectedImage:in:on:)
These calls will provide a menuItems
array of MenuItem
objects. These are like UIMenuItem
but allow block-based calls and images. You can return instances of either MenuItem
or UIMenuItem
, and you can return nil
to block any of these menus from appearing.
⚠️ Warning:
UIMenuController
is a shared object andPDFViewController
uses it extensively. Manually setting properties likemenuItems
on it won’t work, as logic in the controller will most likely override it before it’s presented. Instead, use the delegates to customizeUIMenuController
.
The image below shows how the menu for text selection usually appears.

The annotation types offered here for creation are based on the values of the currently active PDFConfiguration
in editableAnnotationTypes
. By default, this includes all annotation types.
To customize this, use the pdfViewController(_:shouldShow:atSuggestedTargetRect:forSelectedText:in:on:)
delegate. You can also disable certain types of menu actions via the allowedMenuActions
property.
API | Use |
---|---|
.search |
Allow search from selected text. |
.define |
Offers to show a menu item with the “Define” title on selected text. Not available for Mac Catalyst. |
.wikipedia |
Offers a toggle for Wikipedia. |
.speak |
Allows text-to-speech. |
.share |
Allows sharing content. Also used for image selection. |
.copy |
Allows copying the selected text. |
.markup |
Allows marking up the selected text (highlight/underline/strikeout). |
.redact |
Allows redacting the selected text. |
.createLink |
Allows creating a link from the selected text. |
.annotationCreation |
Helper that encapsulates all annotation creation menu action types. |
.all |
All text selection entries. |
Note that if you want to remove certain menu items, as a general rule, filter out the unwanted items rather than maintaining a list of items you want to keep:
func pdfViewController(_ pdfController: PDFViewController, shouldShow menuItems: [MenuItem], atSuggestedTargetRect rect: CGRect, forSelectedText selectedText: String, in textRect: CGRect, on pageView: PDFPageView) -> [MenuItem] { // Filter out the "Copy" menu item. return menuItems.filter() {$0.identifier != TextMenu.copy.rawValue} }
- (NSArray<PSPDFMenuItem *> *)pdfViewController:(PSPDFViewController *)pdfController shouldShowMenuItems:(NSArray<PSPDFMenuItem *> *)menuItems atSuggestedTargetRect:(CGRect)rect forAnnotations:(NSArray<PSPDFAnnotation *> *)annotations inRect:(CGRect)annotationRect onPageView:(PSPDFPageView *)pageView { // Filter out the "Copy" menu item. return [menuItems filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(PSPDFMenuItem *menuItem, NSDictionary *bindings) { return ![menuItem.identifier isEqualToString:PSPDFTextMenuCopy]; }]]; }
Make sure to filter using properties that aren’t localization dependent. The identifier
property of the MenuItem
object isn’t localized and is therefore a good choice:
public func pdfViewController(_ pdfController: PDFViewController, shouldShow menuItems: [MenuItem], atSuggestedTargetRect rect: CGRect, forSelectedText selectedText: String, in textRect: CGRect, on pageView: PDFPageView) -> [MenuItem] { // Disable Wikipedia. // Note that for words that are in the iOS dictionary instead of Wikipedia, we show the "Define" menu item with the native dictionary UI. // There's also a simpler way to disable Wikipedia (see `TextSelectionMenuAction `). var newMenuItems = menuItems.filter { $0.identifier != TextMenu.wikipedia.rawValue } guard let query = selectedText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let queryURL = URL(string: String(format: "https://www.google.com/search?q=%@", arguments: [query])) else { return newMenuItems } // Add the option to google for the currently selected text. let googleItem = MenuItem(title: NSLocalizedString("Google", comment: ""), block: { // Create browser. let browser = WebViewController(url: queryURL) browser.delegate = pdfController browser.modalPresentationStyle = .popover browser.preferredContentSize = CGSize(width: 600, height: 500) let presentationOptions = [ .sourceRect: NSValue(cgRect: textRect), .inNavigationController: true, .closeButton: true ] as [PresentationOption : Any] pdfController.present(browser, options: presentationOptions, animated: true, sender: nil, completion: nil) }, identifier: "Google") newMenuItems.append(googleItem) return newMenuItems }
- (NSArray<PSPDFMenuItem *> *)pdfViewController:(PSPDFViewController *)pdfController shouldShowMenuItems:(NSArray<PSPDFMenuItem *> *)menuItems atSuggestedTargetRect:(CGRect)rect forSelectedText:(NSString *)selectedText inRect:(CGRect)textRect onPageView:(PSPDFPageView *)pageView { // Disable Wikipedia. // Be sure to check for the `PSPDFMenuItem` class; there might also be classic `UIMenuItem`s in the array. // Note that for words that are in the iOS dictionary instead of Wikipedia, we show the "Define" menu item with the native dictionary UI. // There's also a simpler way to disable Wikipedia (see `PSPDFTextSelectionMenuAction`). NSMutableArray *newMenuItems = [menuItems mutableCopy]; for (PSPDFMenuItem *menuItem in menuItems) { if ([menuItem isKindOfClass:PSPDFMenuItem.class] && [menuItem.identifier isEqualToString:PSPDFTextMenuWikipedia]) { [newMenuItems removeObjectIdenticalTo:menuItem]; break; } } // Add the option to google for the currently selected text. PSPDFMenuItem *googleItem = [[PSPDFMenuItem alloc] initWithTitle:NSLocalizedString(@"Google", nil) block:^{ NSString *URLString = [NSString stringWithFormat:@"https://www.google.com/search?q=%@", [selectedText stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]]; // Create browser. PSPDFWebViewController *browser = [[PSPDFWebViewController alloc] initWithURL:(NSURL *)[NSURL URLWithString:URLString]]; browser.delegate = pdfController; browser.modalPresentationStyle = UIModalPresentationPopover; browser.preferredContentSize = CGSizeMake(600.f, 500.f); NSDictionary<PSPDFPresentationOption, id> *presentationOptions = @{ PSPDFPresentationOptionSourceRect: @(textRect), PSPDFPresentationOptionInNavigationController: @YES, PSPDFPresentationOptionCloseButton: @YES }; [pdfController presentViewController:browser options:presentationOptions animated:YES sender:nil completion:NULL]; } identifier:@"Google"]; [newMenuItems addObject:googleItem]; return newMenuItems; }
For more details, take a look at PSCSimpleAnnotationInspectorExample
from our Catalog app.
Additional Menu Item Customizations
You can customize a MenuItem
object so that it doesn’t close the menu automatically on tapping. Custom menu items can be hot-swapped when they’re tapped, depending on the current state of the app, without the UIMenuController
disappearing in between.
For example, maybe you want to add a Play button to the menu and have it change to Pause when it’s tapped and back to Play when tapped again. And maybe you also require the menu to remain open while this is happening. To accomplish this, use any of the shouldShowMenuItems
delegate methods and add your custom MenuItem
in the following way:
// Enum to keep track of the current state. enum PlayerState { case playing case paused } func pdfViewController(_ pdfController: PDFViewController, shouldShow menuItems: [MenuItem], atSuggestedTargetRect rect: CGRect, forSelectedText selectedText: String, in textRect: CGRect, on pageView: PDFPageView) -> [MenuItem] { let playItem = MenuItem(title: "Play", block: { // Update the state and anything else this button was actually supposed to do. self.state = .playing // Fetch the menu items again for the newly updated state. UIMenuController.shared.menuItems = self.pdfViewController(pdfController, shouldShow: menuItems, atSuggestedTargetRect: rect, forSelectedText: selectedText, in: textRect, on: pageView) // This presents the menu again after tapping on the custom item. UIMenuController.shared.hideMenu(from: pageView) UIMenuController.shared.showMenu(from: pageView, rect: rect) }, identifier: "Play") let pauseItem = MenuItem(title: "Pause", block: { // Update the state and anything else this button was actually supposed to do. self.state = .paused // Fetch the menu items again for the newly updated state. UIMenuController.shared.menuItems = self.pdfViewController(pdfController, shouldShow: menuItems, atSuggestedTargetRect: rect, forSelectedText: selectedText, in: textRect, on: pageView) // This presents the menu again after tapping on the custom item. UIMenuController.shared.hideMenu(from: pageView) UIMenuController.shared.showMenu(from: pageView, rect: rect) }, identifier: "Pause") // Depending on the current state, add the play/pause item at the beginning of the menu items array. var newMenuItems = menuItems if self.state == .paused { newMenuItems = [playItem] + newMenuItems } else if self.state == .playing { newMenuItems = [pauseItem] + newMenuItems } return newMenuItems }
// Enum to keep track of the current state. typedef NS_ENUM(NSInteger, PlayerState) { PlayerStatePlaying, PlayerStatePaused }; - (NSArray<PSPDFMenuItem *> *)pdfViewController:(PSPDFViewController *)pdfController shouldShowMenuItems:(NSArray<PSPDFMenuItem *> *)menuItems atSuggestedTargetRect:(CGRect)rect forSelectedText:(NSString *)selectedText inRect:(CGRect)textRect onPageView:(PSPDFPageView *)pageView { PSPDFMenuItem *playItem = [[PSPDFMenuItem alloc] initWithTitle:@"Play" block:^{ // Update the state and anything else this button was actually supposed to do. self.state = PlayerStatePlaying; // Fetch the menu items again for the newly updated state. UIMenuController.sharedMenuController.menuItems = [self pdfViewController:pdfController shouldShowMenuItems:menuItems atSuggestedTargetRect:rect forSelectedText:selectedText inRect:textRect onPageView:pageView]; // This presents the menu again after tapping on the custom item. [UIMenuController.sharedMenuController hideMenuFromView:pageView]; [UIMenuController.sharedMenuController showMenuFromView:pageView rect:rect]; } identifier:@"Play"]; PSPDFMenuItem *pauseItem = [[PSPDFMenuItem alloc] initWithTitle:@"Pause" block:^{ // Update the state and anything else this button was actually supposed to do. self.state = PlayerStatePaused; // Fetch the menu items again for the newly updated state. UIMenuController.sharedMenuController.menuItems = [self pdfViewController:pdfController shouldShowMenuItems:menuItems atSuggestedTargetRect:rect forSelectedText:selectedText inRect:textRect onPageView:pageView]; // This presents the menu again after tapping on the custom item. [UIMenuController.sharedMenuController hideMenuFromView:pageView]; [UIMenuController.sharedMenuController showMenuFromView:pageView rect:rect]; } identifier:@"Pause"]; // Depending on the current state, add the play/pause item at the beginning of the menu items array. NSMutableArray *newMenuItems = [menuItems mutableCopy]; if (self.state == PlayerStatePaused) { [newMenuItems insertObject:playItem atIndex:0]; } else if (self.state == PlayerStatePlaying) { [newMenuItems insertObject:pauseItem atIndex:0]; } return newMenuItems; }