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:presentMenu:animated:) 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(_:menuForAnnotations:onPageView:appearance:suggestedMenu:) 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.

Information

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:in:) 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(_:menuForCreatingAnnotationAt:onPageView:appearance:suggestedMenu:) 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(_:menuForText:onPageView:appearance:suggestedMenu:) 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

Information

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:presentMenu:animated:) 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(_:menuForImage:onPageView:appearance:suggestedMenu:) 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.

Information

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.

Information

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.