Customizing Menus

PSPDFKit uses UIMenuController for context-sensitive menus when you select text, images, or annotations or long press on an empty space. There are several delegates in PSPDFViewControllerDelegate to customize this behavior:

These calls will provide a menuItems array of PSPDFMenuItem objects. These are like UIMenuItem but allow block-based calls and images. You can return instances of either PSPDFMenuItem or UIMenuItem, and you can return nil to block any of these menus from appearing.

⚠️ Warning: UIMenuController is a shared object and PSPDFViewController uses it extensively. Manually setting properties like menuItems on it will not work, as logic in the controller will most likely override it before it is 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 PSPDFConfiguration in editableAnnotationTypes. By default, this includes all annotation types.

To customize this, use the pdfViewController:shouldShowMenuItems:atSuggestedTargetRect:forSelectedText:inRect:onPageView: delegate. You can also disable certain types of menu actions via the allowedMenuActions property:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// These are the menu options when text is selected on this document.
public struct PSPDFTextSelectionMenuAction : OptionSet {
    public init(rawValue: UInt)

    /// Allow search from selected text.
    public static var search: PSPDFTextSelectionMenuAction { get }
    /// Offers to show the "Define" menu item on selected text.
    public static var define: PSPDFTextSelectionMenuAction { get }
    /// Offers a toggle for Wikipedia.
    public static var wikipedia: PSPDFTextSelectionMenuAction { get }
    /// Allows text-to-speech.
    public static var speak: PSPDFTextSelectionMenuAction { get }
    public static var all: PSPDFTextSelectionMenuAction { get }
}
Copy
1
2
3
4
5
6
7
8
/// These are the menu options when text is selected on this document.
typedef NS_OPTIONS(NSUInteger, PSPDFTextSelectionMenuAction) {
    PSPDFTextSelectionMenuActionSearch    = 1 << 0, /// Allow search from selected text.
    PSPDFTextSelectionMenuActionDefine    = 1 << 1, /// Offers to show the "Define" menu item on selected text.
    PSPDFTextSelectionMenuActionWikipedia = 1 << 2, /// Offers a toggle for Wikipedia.
    PSPDFTextSelectionMenuActionSpeak     = 1 << 3, /// Allows text-to-speech.
    PSPDFTextSelectionMenuActionAll       = NSUIntegerMax
};

The menu delegate for annotations is pdfViewController:shouldShowMenuItems:atSuggestedTargetRect:forAnnotations:inRect:onPageView:. If this is called with nil as the annotation argument, the menu to create new annotations will be shown (and in that case, annotationRect will also be nil).

Note that if you want to remove certain menu items, as a general rule, you should filter out the unwanted items in a blacklist:

Copy
1
2
3
4
func pdfViewController(_ pdfController: PSPDFViewController, shouldShow menuItems: [PSPDFMenuItem], atSuggestedTargetRect rect: CGRect, forSelectedText selectedText: String, in textRect: CGRect, on pageView: PSPDFPageView) -> [PSPDFMenuItem] {
    // Filter out the "Copy" menu item.
    return menuItems.filter() {$0.identifier != PSPDFTextMenu.copy.rawValue}
}
Copy
1
2
3
4
5
6
- (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];
    }]];
}

If you try to whitelist menu items, you might break functionality. The identifier property of the PSPDFMenuItem object is not localized and is perfect for comparison:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public func pdfViewController(_ pdfController: PSPDFViewController, shouldShow menuItems: [PSPDFMenuItem], atSuggestedTargetRect rect: CGRect, forSelectedText selectedText: String, in textRect: CGRect, on pageView: PSPDFPageView) -> [PSPDFMenuItem] {
    // 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 is also a simpler way to disable Wikipedia (see `PSPDFTextSelectionMenuAction`).
    var newMenuItems = menuItems.filter { $0.identifier != PSPDFTextMenuWikipedia }

    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 = PSPDFMenuItem(title: NSLocalizedString("Google", comment: ""), block: {
        // Create browser.
        let browser = PSPDFWebViewController(url: queryURL)
        browser.delegate = pdfController
        browser.modalPresentationStyle = .popover
        browser.preferredContentSize = CGSize(width: 600, height: 500)

        let presentationOptions: [String : Any] = [
            PSPDFPresentationRectKey: NSValue(cgRect: textRect),
            PSPDFPresentationInNavigationControllerKey: true,
            PSPDFPresentationCloseButtonKey: true
        ]

        pdfController.present(browser, options: presentationOptions, animated: true, sender: nil, completion: nil)
        }, identifier: "Google")

    newMenuItems.append(googleItem)
    return newMenuItems
}
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- (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 `UIMenuItems` 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 is 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 *presentationOptions = @{
            PSPDFPresentationRectKey: BOXED(textRect),
            PSPDFPresentationInNavigationControllerKey: @YES,
            PSPDFPresentationCloseButtonKey: @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 PSPDFMenuItem object so that it does not close the menu automatically on tapping. Custom menu items can be hot-swapped when they are tapped, depending on the current state of the app, without the UIMenuController disappearing in between.

For example, suppose you want to add a Play button to the menu, having it change to Pause when it is tapped and back to Play when tapped again. And suppose you also require the menu to remain open all while this is happening. To accomplish this, you can use any of the shouldShowMenuItems delegate methods and add your custom PSPDFMenuItem like this:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Enum to keep track of the current state.
enum PlayerState {
    case playing
    case paused
}

func pdfViewController(_ pdfController: PSPDFViewController, shouldShow menuItems: [PSPDFMenuItem], atSuggestedTargetRect rect: CGRect, forSelectedText selectedText: String, in textRect: CGRect, on pageView: PSPDFPageView) -> [PSPDFMenuItem] {
    let playItem = PSPDFMenuItem(title: "Play", block: {
        // Update the state and anything else that 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 line keeps the menu open after tapping on the custom item.
        UIMenuController.shared.isMenuVisible = true
    }, identifier: "Play")

    let pauseItem = PSPDFMenuItem(title: "Pause", block: {
        // Update the state and anything else that 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 line keeps the menu open after tapping on the custom item.
        UIMenuController.shared.isMenuVisible = true
    }, 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
}
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 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 that 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 line keeps the menu open after tapping on the custom item.
        UIMenuController.sharedMenuController.menuVisible = YES;
    } identifier:@"Play"];

    PSPDFMenuItem *pauseItem = [[PSPDFMenuItem alloc] initWithTitle:@"Pause" block:^{
        // Update the state and anything else that 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 line keeps the menu open after tapping on the custom item.
        UIMenuController.sharedMenuController.menuVisible = YES;
    } 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;
}

Creating Annotations

When long tapping on empty space, we will call the forAnnotations: variant without any annotations — this is to show the create annotations menu. There’s a convenience property called createAnnotationMenuEnabled, which can be used to disable this feature in PSPDFConfiguration, and you can also customize what types should be displayed there using createAnnotationMenuGroups:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let controller = PSPDFViewController(document: document, configuration: PSPDFConfiguration { builder in
    builder.isCreateAnnotationMenuEnabled = false
    builder.createAnnotationMenuGroups = [
        PSPDFAnnotationGroup(items: [
            PSPDFAnnotationGroupItem(type: .freeText),
            PSPDFAnnotationGroupItem(type: .signature),
            PSPDFAnnotationGroupItem(type: .note)
        ]),
        PSPDFAnnotationGroup(items: [
            PSPDFAnnotationGroupItem(type: .ink, variant: .inkPen, configurationBlock: PSPDFAnnotationGroupItem.inkConfigurationBlock())
        ]),
        PSPDFAnnotationGroup(items: [
            PSPDFAnnotationGroupItem(type: .ink, variant: .inkHighlighter, configurationBlock: PSPDFAnnotationGroupItem.inkConfigurationBlock())
        ]),
        PSPDFAnnotationGroup(items: [
            PSPDFAnnotationGroupItem(type: .image),
            PSPDFAnnotationGroupItem(type: .stamp),
            PSPDFAnnotationGroupItem(type: .sound)
        ]),
        PSPDFAnnotationGroup(items: [
            PSPDFAnnotationGroupItem(type: .eraser)
        ])
    ]
})
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
PSPDFViewController *controller = [[PSPDFViewController alloc] initWithDocument:document configuration:[PSPDFConfiguration configurationWithBuilder:^(PSPDFConfigurationBuilder *builder) {
    builder.createAnnotationMenuEnabled = NO;
    builder.createAnnotationMenuGroups = @[
      [PSPDFAnnotationGroup groupWithItems:@[
        [PSPDFAnnotationGroupItem itemWithType:PSPDFAnnotationStringFreeText],
        [PSPDFAnnotationGroupItem itemWithType:PSPDFAnnotationStringSignature],
        [PSPDFAnnotationGroupItem itemWithType:PSPDFAnnotationStringNote]
      ]],
      [PSPDFAnnotationGroup groupWithItems:@[
        [PSPDFAnnotationGroupItem itemWithType:PSPDFAnnotationStringInk
          variant:PSPDFAnnotationStringInkPen
          configurationBlock:[PSPDFAnnotationGroupItem inkConfigurationBlock]]
      ]],
      [PSPDFAnnotationGroup groupWithItems:@[
        [PSPDFAnnotationGroupItem itemWithType:PSPDFAnnotationStringInk
          variant:PSPDFAnnotationStringInkHighlighter
          configurationBlock:[PSPDFAnnotationGroupItem inkConfigurationBlock]]
      ]],
      [PSPDFAnnotationGroup groupWithItems:@[
        [PSPDFAnnotationGroupItem itemWithType:PSPDFAnnotationStringImage],
        [PSPDFAnnotationGroupItem itemWithType:PSPDFAnnotationStringStamp],
        [PSPDFAnnotationGroupItem itemWithType:PSPDFAnnotationStringSound]
      ]],
      [PSPDFAnnotationGroup groupWithItems:@[
        [PSPDFAnnotationGroupItem itemWithType:PSPDFAnnotationStringEraser]
      ]]
    ];
}]];