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: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, 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: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.

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 {
    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.

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.

Text and Image Selection Menus

Warning

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:

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 and PDFViewController uses it extensively. Manually setting properties like menuItems on it won’t work, as logic in the controller will most likely override it before it’s presented. Instead, use the delegates to customize UIMenuController.

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;
}