22

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!

Jordan Smith
  • 10,310
  • 7
  • 68
  • 114
  • maybe will help you a bit http://ios-blog.co.uk/tutorials/rich-text-editing-highlighting-and-uimenucontroller-part-3/ and http://stackoverflow.com/a/3286756/1702413 – TonyMkenu Jan 30 '13 at 07:38
  • @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 Jan 30 '13 at 08:23

2 Answers2

32

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

Community
  • 1
  • 1
Fabian Kreiser
  • 8,307
  • 1
  • 34
  • 60
  • Thanks, this is on the right track! A couple of problems I've run into, if anyone's got ideas how to work around these then please say. 1. The notification for when the menu controller is shown sometimes seems to be slightly delayed. If I try to intercept the menu controller being shown like with the code above and show nothing instead, occasionally it pops up for a brief second before disappearing. I haven't figured out how to reproduce this, but have noticed it several times. Not too big of an issue as it does disappear pretty quick, but any way of stopping this wee glitch would be great! – Jordan Smith Feb 07 '13 at 22:36
  • 2. Location of the custom controller's arrow. The menu frame that you get is a good start, but also the direction of the arrow needs to be known. The arrowDirection property just gives a 'default' value which doesn't help. Not just the arrow's direction needs to be known actually, it's position does as well. The arrow should be centred on selected text, and due to the menu controller's origin being constrained to the screen width, then the arrow often isn't centred in the middle of the menu. How can I find the correct arrow position? – Jordan Smith Feb 07 '13 at 22:43
  • 1) The only way I can think of solving that would be to use custom gesture recognizers that reimplement UIMenuController's behavior. One for long presses, another one for taps on selected text, etc. This will be a quite cumbersome process, though. – Fabian Kreiser Feb 08 '13 at 07:14
  • 2) You'll have to use your own logic to determine how the arrow should be positioned. Determining whether it's at the top or at the bottom is quite easy by looking at the menuFrame's center. If the y value is higher than, let's say, your menu's height plus 5 points, display it at the top, otherwise at the bottom. Whether the arrow is centered or moved to the right or left will be more complex. To start I'd use something like that: `targetCenter = targetRect.origin.x + targetRect.size.width / 2; arrowXPosition = floor(targetCenter - (targetCenter - menuSize.width / 2));` – Fabian Kreiser Feb 08 '13 at 07:25
  • 1 - I could be wrong, but if I reimplement these, will that stop text from being selected? 2 - I'm still not sure how you plan to figure out where the centre of the selected text is - top and bottom is OK but it's finding the centre of the selected text that I can't see a way of doing using just the given menu frame. – Jordan Smith Feb 08 '13 at 07:52
  • No, don't reimplement the text selection, only add additional gesture recognizers to reimplement UIMenuController's behavior in order to determine when to show your menu and disable UIMenuController completely. For getting the frame of the selected text, you can use the `UITextInput` protocol, like in this answer: http://stackoverflow.com/a/8684006/322548 – Fabian Kreiser Feb 08 '13 at 07:58
3

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

yijiankaka
  • 128
  • 3
  • Thanks, I've seen this already though. I need a menu controller that's twice as high, this doesn't help with that - it just uses some hackish code to replace the text in a UIMenuController with images. – Jordan Smith Jan 30 '13 at 02:47