4
votes

I am developing an app that supports editing attributedText in a UITextView. To provide the tools to the user for formatting their input, I am using an inputAccessoryView to augment the keyboard with options like bullet list, numbered list, indent, outdent, font controls (bold, underline, increase fontsize, decrease font size), etc. This is too much to fit on the inputAccessoryView so I am looking to use the UIMenuController to provide a mechanism to provide more space for the user to make their intentions known.

So, I have a inputAccessoryView with a 'listAccessory' button. When it is pressed, I wanted to show a UIMenuController with four options (bullet, number, increase indent, decrease indent). But when I show this menu, it ALSO includes 'select', 'selectAll' and 'paste'.

I do not have any of these methods (select:, selectAll:, or paste: as defined in the UIResponderStandardEditActions informal protocol) defined in my view. I have defined canPerformAction:withSender: and only respond with 'YES' for my selectors.

- (BOOL) canPerformAction:(SEL)selector withSender:(id) sender
{
    DDLogInfo(@"canPerformAction: %@", NSStringFromSelector(selector));

    if (selector == @selector(formatAsBulletList:)) return YES;
    if (selector == @selector(formatAsNumberedList:)) return YES;
    if (selector == @selector(formatIncreaseIndent:)) return YES;
    if (selector == @selector(formatDecreaseIndent:)) return YES;

    return NO; // return [super canPerformAction:selector withSender:sender];
}

When I log the selectors being called in this code, I don't see any request for 'select:', 'selectAll:', or 'paste:', so I believe that the UIMenuController code is testing for those methods with direct calls to canPerformSelector() against the class.

Since I don't implement those functions in my viewController (derived from UITableViewController), I can only believe surmise that the UIMenuController is looking up the responder chain and seeing that the responder that initiatated the keyboard initially is a UITextView, which DOES support select, selectAll, and paste.

So I have a couple of questions:

1) is my understanding of the situation right?

2) how do I force those menu items to not appear? Can I somehow temporarily break the responder chain without dismissing the keyboard?

1

1 Answers

1
votes

Cool question. The problem is as you understand it .The UITextView IS the first responder when you try to invoke your menu so it populates the menu with the Select and Select All actions

A solution is to subclass UITextView and add an extra property which allows you to block the items briefly.

I tried this and it works on UITextField as its what i had to hand but theres no reason to believe it won't work on UITextView

Subclass your view lightly.

@interface CharlieDevTextView : UITextView

@property BOOL blockActionMenu;

@end

And

@implementation CharlieDevTextView

-(BOOL)canPerformAction:(SEL)action withSender:(id)sender {

    if (self.blockActionMenu) {
        return NO;
    }
    return [super canPerformAction:action withSender:sender];

}

@end

then when you construct your menu (assuming you have an IBOutlet to the textview)

-(void)yellowMellow:(id)sender {

}


-(void)createMenuForButton:(UIButton *)sender
{
    UIMenuItem *newInstanceItem = [[UIMenuItem alloc] initWithTitle:@"Woot" action:@selector(yellowMellow:)];

    [UIMenuController sharedMenuController].menuItems = @[newInstanceItem];

    CGPoint apoint = sender.center;

    self.charlieTextView.blockActionMenu = YES;

    [[UIMenuController sharedMenuController] setTargetRect:CGRectMake(apoint.x,apoint.y, 0, 0) inView:sender.superview];

    [[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES];

    self.charlieTextView.blockActionMenu = NO;
}

Alternatively listen to the UIMenuControllerWillShowMenuNotification and UIMenuControllerDidHideMenuNotification for switching blocking on and off.

and possibly to make it slightly less ugly create a delegate rather than a property. Your view controller will be the delegate and predicate on whether it is about to show the menu or noticed that the menu has been dismissed.

Essentially same effect , different pattern.

@protocol CharlieTextViewMenuDelegate <NSObject>

-(BOOL)shouldBlockMenu;

@end

@interface CharlieTextView : UITextView

@property (nonatomic,weak) id< CharlieTextViewMenuDelegate> menuDelegate;

@end

@implementation CharlieDevTextView

-(BOOL)canPerformAction:(SEL)action withSender:(id)sender {

    if ([self.menuDelegate shouldBlockMenu]) {
        return NO;
    }
    return [super canPerformAction:action withSender:sender];

}

@end