Customizing menus on iOS
Nutrient 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, Nutrient 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, Nutrient 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.
Text selection menu
When you select text by a long press or a right click, Nutrient presents a menu. This section explains how to present and customize this menu in your project.
Presenting the text selection menu
The text selection menu can only be presented in response to user interaction. Programmatically presenting this menu isn’t possible.
Customizing the text selection menu
Implement the pdfViewController(_:
delegate method to customize the text selection menu directly:
func pdfViewController(_ sender: PDFViewController, menuForText glyphs: GlyphSequence, onPageView pageView: PDFPageView, appearance: EditMenuAppearance, suggestedMenu: UIMenu) -> UIMenu { // Return the customized `suggestedMenu`. }
Returning a UIMenu
with no children prevents the text selection menu from being displayed. To disable text selection altogether, it’s recommended to set the isTextSelectionEnabled
configuration property to false
.
See the Common Customization Techniques section to learn more about customizing the UIMenu
object.
Customizing the tools in the text selection menu
To customize which tools are available in the text selection menu, use contentMenuConfiguration
and set the custom closure for the annotationToolChoices
property.
The following example demonstrates how to limit the tools to include only highlight, underline, and redaction annotations:
let configuration = PDFConfiguration { $0.contentMenuConfiguration = ContentMenuConfiguration { $0.annotationToolChoices = { glyphs, pageView, appearance, defaultChoices in [.highlight, .underline, .redaction] } } }
To learn more about configuring PDFViewController
using the PDFConfiguration
object, see the guide on how to configure PDF view controllers.
Image selection menu
This section refers to images that are part of the page content. To customize the menu for image annotations, see the Annotation Selection Menu section instead.
When you select an image by a long press or a right click, Nutrient presents a menu. This section explains how to present and customize this menu in your project.
Presenting the image selection menu
To programmatically present the menu for a selected image, use select(image:
and set the presentMenu
parameter to true
:
let image = pageView.selectionView.selectedImage pageView.select(image: image, presentMenu: true, animated: true)
Customizing the image selection menu
Implement the pdfViewController(_:
delegate method to customize the image selection menu directly:
func pdfViewController(_ sender: PDFViewController, menuForImage image: ImageInfo, onPageView pageView: PDFPageView, appearance: EditMenuAppearance, suggestedMenu: UIMenu) -> UIMenu { // Return the customized `suggestedMenu`. }
Returning a UIMenu
with no children prevents the image selection menu from being displayed. To disable image selection altogether, set the isImageSelectionEnabled
configuration property to false
.
See the common customization techniques section to learn more about how to customize the UIMenu
object.
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 { // The `filterAction` function used here is defined below. suggestedMenu.filterActions { $0 == .PSPDFKit.copy || $0 == .PSPDFKit.delete } }
Check out the API reference for Nutrient’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
.
Here’s the implementation of the filterAction
helper method used above:
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. UIDeferredMenuElement
is also supported.
Inserting submenus
Implement one of the UIMenu
-based delegate methods, and modify the suggestedMenu
parameter to include a custom submenu. Nutrient 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.