How to modify context menu shown in a UITextView?

In my iPad app running under macOS Catalyst I would like to modify the context menu that appears when you right-click on a UITextViiew but I can't figure out how.


I have overridden the buildMenuWithBulder:/buildMenu(with:) method from UIResponder in my view controller and app delegate but none are called when right-clicking on a UITextView.


I have been able to disable a couple of the menu options by subclassing UITextView and overriding the canPerformAction:withSender:/canPerformAction(_:withSender:) and targetForAction:withSender:/target(_:withSender:) methods but I can't remove/disable most of the menu.


I also overrode the validateCommand:/validate(_:) method in my UITextView subclass and that was only called for a small subset of the menus I wish to remove/disable.


The Menus sample app only shows how to modify the app's main menu and how to add a context menu to a view controller but there is no info on how to modify the context menu of a control such as UITextView.


Does anyone know how to do this?


Specifically I want to remove the Show Fonts and Show Colors menus under Font and I wish to remove the whole Substitutions menu. The allowsEditingTextAttributes property of the text view is enabled which is what makes the Font portion of the menu appear. I want to keep the Bold/Italic/Underline toggles.

Replies

I am struggling with this as well! It seems to be possible to add custom items on iOS using UIMenuController.shared.menuItems = [myCustomMenuItem], but when running this on Catalyst, the item doesn't appear in the text view's context menu. There's also the new func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? that suggests customizability but this also doesn't do anything for me on Catalyst. Has anyone found a solution for this?

I recently figured out a way to remove the "Show Fonts" and "Show Colors" menus from the context menu of a UITextView. This only works on iOS 16.0/Mac Catalyst 16.0 or later.

The following is my Objective-C code. Should be easy enough to translate to Swift.

I started by adding new method to UIMenu:

Category declaration:

@interface UIMenu (Additions)

- (UIMenu *)menuByRemovingChildWithAction:(SEL)action;

@end

Category implementation:

@implementation UIMenu (Additions)

- (UIMenu *)menuByRemovingChildWithAction:(SEL)action {
    NSArray<UIMenuElement *> *children = self.children;
    for (NSInteger i = 0; i < children.count; i++) {
        UIMenuElement *element = children[i];
        if ([element isKindOfClass:[UICommand class]]) {
            UICommand *cmd = (UICommand *)element;
            if (cmd.action == action) {
                NSMutableArray *newChildren = [children mutableCopy];
                [newChildren removeObjectAtIndex:i];

                if (newChildren.count == 0) {
                    return nil;
                } else {
                    return [self menuByReplacingChildren:newChildren];
                }
            }
        } else if ([element isKindOfClass:[UIMenu class]]) {
            UIMenu *menu = (UIMenu *)element;
            UIMenu *newMenu = [menu menuByRemovingChildWithAction:action];
            if (newMenu == nil) {
                NSMutableArray *newChildren = [children mutableCopy];
                [newChildren removeObjectAtIndex:i];

                return [self menuByReplacingChildren:newChildren];
            } else if (newMenu != menu) {
                NSMutableArray *newChildren = [children mutableCopy];
                newChildren[i] = newMenu;

                return [self menuByReplacingChildren:newChildren];
            }
        }
    }

    return self;
}

@end

This recursively goes through a menu hierarchy and removes any menu item that is a UICommand with the given action.

With that in place you need to implement the iOS 16.0 UITextViewDelegate method:

- (UIMenu *)textView:(UITextView *)textView editMenuForTextInRange:(NSRange)range suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0)) {
    UIMenu *menu = [UIMenu menuWithChildren:suggestedActions];
    menu = [menu menuByRemovingChildWithAction:NSSelectorFromString(@"toggleFontPanel:")];
    menu = [menu menuByRemovingChildWithAction:NSSelectorFromString(@"orderFrontColorPanel:")];

    return menu;
}

That's it. All other attempts failed. I was able to disable the "Show Fonts" menu by overriding canPerformAction:sender: in the App Delegate. But for some reason it did nothing for the "Show Colors" menu.

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
    if (action == NSSelectorFromString(@"toggleFontPanel:") || action == NSSelectorFromString(@"orderFrontColorPanel:")) {
        return NO;
    }

    BOOL res = [super canPerformAction:action withSender:sender];

    return res;
}