22
votes

When text is selected, by default a UIMenuController pops up with cut/copy/paste etc.

enter image description here

I'd like to replace this with my own custom view (similar looking, but twice as high so that I can have two rows of buttons/custom views). How can I do this?

I know there's no easy way. I'm expecting that if there's an easy solution, it won't be very elegant. The code can't use any private API either.

I'd really, really rather not have to implement my own text view, reimplement text selection and input, and reimplement the magnifying view just so I can write my own UIMenuController clone if there's any way to avoid it. It's pretty important to the app's interface that I can replace the UIMenuController, so if there's no other answer then I may end up doing this. I'll be VERY grateful if anyone can save me a decent chunk of time and propose another, easier way of doing this!

2
@TonyMkenu thanks, but these tutorials just explain how to add custom items to a UIMenuController, as is supported well by Apple provided API - this isn't what I want to do, I want to completely replace UIMenuController with my own custom implementation.Jordan Smith

2 Answers

32
votes

There are three important things you have to know before you can start:

1) You'll have to write your custom menu controller view, but I guess you kinda expected that. I only know of a commercial implementation of a custom menu controller, but this shouldn't be too hard.

2) There is a useful method on UIResponder called -canPerformAction:withSender:. Read more about it in the UIResponder Class Reference. You can use that method to determine whether your text view supports a specific standard action (defined in the UIResponderStandardEditActions protocol).
This will be useful when deciding which items to show in your custom menu controller. For example the Paste menu item will only be shown when the user's pasteboard contains a string to paste.

3) You can detect when the UIMenuController will be shown by listening to the UIMenuControllerWillShowMenuNotification notification.

Now that you know all of that, this is how I'd start tackling that:

1) Listen for UIMenuControllerWillShowMenuNotifications when the text view is first responder

- (void)textViewDidBeginEditing:(UITextView *)textView {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(menuWillBeShown:) name:UIMenuControllerWillShowMenuNotification object:nil];
}

- (void)textViewDidEndEditing:(UITextView *)textView {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIMenuControllerWillShowMenuNotification object:nil];
}

2) Show your custom menu controller instead of the default UIMenuController

- (void)menuWillBeShown:(NSNotification *)notification {
    CGRect menuFrame = [[UIMenuController sharedMenuController] menuFrame];
    [[UIMenuController sharedMenuController] setMenuVisible:NO animated:NO]; // Don't show the default menu controller

    CustomMenuController *controller = ...;
    controller.menuItems = ...;
    // additional stuff goes here

    [controller setTargetRectWithMenuFrame:menuFrame]; // menuFrame is in screen coordinates, so you might have to convert it to your menu's presenting view/window/whatever

    [controller setMenuVisible:YES animated:YES];
}

Misc. 1) You can use a fullscreen UIWindow for showing your custom menu so it can overlap the status bar.

UIWindow *presentingWindow = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
presentingWindow.windowLevel = UIWindowLevelStatusBar + 1;
presentingWindow.backgroundColor = [UIColor clearColor];

[presentingWindow addSubview:controller];
[presentingWindow makeKeyAndVisible];

Misc. 2) For determining which menu items to show you can use the mentioned -canPerformAction:withSender:

BOOL canPaste = [textView canPerformAction:@selector(paste:) withSender:nil];
BOOL canSelectAll = [textView canPerformAction:@selector(selectAll:) withSender:nil];

Misc. 3) You'll have to handle dismissing the menu yourself by using a UITapGestureRecognizer on the presenting window or something like that.

This won't be easy, but it's doable and I hope it works out well for you. Good luck!

Update:
A new menu implementation popped up on cocoacontrols.com today that you might want to check out: https://github.com/questbeat/QBPopupMenu

Update 2:
As explained in this answer you can get the frame of a text view's selected text using -caretRectForPosition:.

3
votes

I think this may help you https://github.com/cxa/UIMenuItem-CXAImageSupport

UIMenuItem uses UILabel to display its title, that means we can swizzle -drawTextInRect: to support image.

UIMenuItem+CXAImageSupport is a dirty hack but should be safe in most cases. Contains no any private API.

Make a category instead of subclassing for UIMenuItem gains more flexibility. Yes, this category can be applied to the awesome PSMenuItem too!

enter image description here