Im employing EGOTextView to do NSAttributedString editing. There is a bug on UIMenuController which I couldn't get over. As the logic described in the following code, the UIMenu will alternately show and hide after a tap on the EGOTextView. But the fact is the [UIMenuController sharedMenuController].isMenuVisible will always get NO, that the code [menuController setMenuVisible:NO animated:NO]; will never get called. Is there any tricky thing I have missed? Thanks for any suggestion!
EGOTextView.m
- (void)tap:(UITapGestureRecognizer*)gesture {
NSLog(#"[UIMenuController sharedMenuController].isMenuVisible :%d",[UIMenuController sharedMenuController].isMenuVisible);
// It always log 0 even if the UIMenu is visible on the screen.
if (_editable && ![self isFirstResponder]) {
[self becomeFirstResponder];
return;
}
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(showMenu) object:nil];
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(showCorrectionMenu) object:nil];
self.correctionRange = NSMakeRange(NSNotFound, 0);
if (self.selectedRange.length>0) {
self.selectedRange = NSMakeRange(_selectedRange.location, 0);
}
NSInteger index = [self closestWhiteSpaceIndexToPoint:[gesture locationInView:self]];
if (_delegateRespondsToDidSelectURL && !_editing) {
if ([self selectedLinkAtIndex:index]) {
return;
}
}
UIMenuController *menuController = [UIMenuController sharedMenuController];
NSLog(#"menuController.menuVisible :%d", menuController.menuVisible);
if ([menuController isMenuVisible]) {
// never run into here
[menuController setMenuVisible:NO animated:NO];
} else {
if (index==self.selectedRange.location) {
[self performSelector:#selector(showMenu) withObject:nil afterDelay:0.35f];
} else {
if (_editing) {
[self performSelector:#selector(showCorrectionMenu) withObject:nil afterDelay:0.35f];
}
}
}
[self.inputDelegate selectionWillChange:self];
self.markedRange = NSMakeRange(NSNotFound, 0);
self.selectedRange = NSMakeRange(index, 0);
[self.inputDelegate selectionDidChange:self];
}
I found the code below, and [menuController setMenuVisible:NO animated:NO]; should be the point that set UIMenuController invisible.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
if ([gestureRecognizer isKindOfClass:NSClassFromString(#"UIScrollViewPanGestureRecognizer")]) {
UIMenuController *menuController = [UIMenuController sharedMenuController];
if ([menuController isMenuVisible]) {
[menuController setMenuVisible:NO animated:NO];
}
}
return NO;
}
The function - (void)tap:(UITapGestureRecognizer*)gesture is run after - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer .
Maybe you should try to search "setMenuVisible:NO". It must also be written at somewhere else.
I m using UISearchBar,and searching is working perfectly but after search clicking on cancel button, didSelectedRowAtIndexPath is not working in UItableView.It is not allowing me to select any row to go further.
Any Idea?
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
{
isSearchOn = YES;
canSelectRow = NO;
table.scrollEnabled = NO;
}
- (void) doneSearching:(id)sender
{
isSearchOn = NO;
canSelectRow = YES;
table.scrollEnabled = YES;
self.navigationItem.rightBarButtonItem = nil;
[table reloadData];
}
- (void)searchBar:(UISearchBar *)searchBar
textDidChange:(NSString *)searchText
{
if ([searchText length] > 0) {
isSearchOn = YES;
canSelectRow = YES;
table.scrollEnabled = YES;
[self searchMoviesTableView:searchText];
}
else {
isSearchOn = NO;
canSelectRow = NO;
table.scrollEnabled = NO;
}
[table reloadData];
}
- (void) searchMoviesTableView :(NSString*)searchText{
[searchResult removeAllObjects];
for (NSDictionary *artistDic in listOfMovies) {
NSRange titleResultRange = [[artistDic valueForKey:#"artist"] rangeOfString:searchText options:(NSCaseInsensitiveSearch | NSLiteralSearch)];
if (titleResultRange.length > 0)
[searchResult addObject:artistDic];
}
}
-(void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
searchBar.text = #"";
table.scrollEnabled = true;
[table reloadData];
[searchBar resignFirstResponder];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
}
here is my mistake...
else {
isSearchOn = NO;
canSelectRow = NO;
table.scrollEnabled = NO;
}
set canSelectRow=YES;
I noticed that you are calling:
searchBar.text = #"";
in searchBarCancelButtonClicked:, which may lead to another call of searchBar:textDidChange:. In searchBar:textDidChange: you set:
canSelectRow = NO;
for strings whose length is 0 (which is true for #""). I wonder is that the root cause?
I have subclassed UILabel to provide a copy menu and would like to add some type of effect that makes the UILabel stand out when this menu is displayed.
Right now I am trying to add and remove a border. It works fine however if the user touches the label and then touches outside of the label the border won't disappear although the copy menu does.
After adding some NSLog's it seems like resignfirstresponder is not being called when this occurs. What happens in the responder chain when this happens and how can I get the border to disappear in this event?
Code as follows :
#implementation CopyLabel
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if(action == #selector(copy:)) {
return YES;
}
else {
return [super canPerformAction:action withSender:sender];
}
}
- (BOOL) canBecomeFirstResponder {
return YES;
}
- (BOOL)becomeFirstResponder {
if([super becomeFirstResponder]) {
self.highlighted = YES;
UIMenuController *menu = [UIMenuController sharedMenuController];
[menu setTargetRect:self.bounds inView:self];
[menu setMenuVisible:YES animated:YES];
return YES;
}
return NO;
}
- (BOOL)resignFirstResponder {
if([super resignFirstResponder]) {
self.highlighted = NO;
UIMenuController *menu = [UIMenuController sharedMenuController];
[menu setMenuVisible:NO animated:YES];
[menu update];
NSLog(#"Resign");
return true;
}
return false;
}
- (void)copy:(id)sender {
UIPasteboard *board = [UIPasteboard generalPasteboard];
[board setString:self.text];
self.highlighted = NO;
[self resignFirstResponder];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if([self isFirstResponder]) {
[self resignFirstResponder];
}
else if([self becomeFirstResponder]) {
} else {
[self resignFirstResponder];
}
}
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
self.layer.borderColor = [UIColor blueColor].CGColor;
self.layer.borderWidth = 0.0;
if(self.highlighted) {
self.layer.borderWidth = 1.0;
}
}
#end
UIMenuController posts a UIMenuControllerDidHideMenuNotification. When you listen for that notification (using NSNotificationCenter) you can send resignFirstResponder to your Label at the right time.
Example:
- (id)init... {
...
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(editMenuHidden)
name:UIMenuControllerDidHideMenuNotification
object:nil];
...
}
- (void)dealloc {
...
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIMenuControllerDidHideMenuNotification
object:nil];
...
}
- (void)editMenuHidden {
[self resignFirstResponder];
}
...
I have UILabel and I need it to be able support copy&paste (actually only copy as it is read only). I subclassed UILabel to support copy and it works fine. I also added text highlight so user knows what exactly is he copying when he clicks on label.
My problem is that I don't know how to cancel that highlight when user clicks somewhere else. Copy bubble disappears, but I don't get any callback so text remains highlighted. Is there special callback I missed that I can use or do I have to come up with some dirty hack? Or is there perhaps more standard way of highlighting text in UILabel that I am not aware of that would be handled automatically like Copy bubble?
Here my custom UILabel code:
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if(action == #selector(copy:)) {
return YES;
}
else {
return [super canPerformAction:action withSender:sender];
}
}
- (BOOL)becomeFirstResponder {
if([super becomeFirstResponder]) {
self.highlighted = YES;
UIMenuController *menu = [UIMenuController sharedMenuController];
[menu setTargetRect:self.bounds inView:self];
[menu setMenuVisible:YES animated:YES];
return YES;
}
return NO;
}
- (BOOL)resignFirstResponder {
if([super resignFirstResponder]) {
self.highlighted = NO;
UIMenuController *menu = [UIMenuController sharedMenuController];
[menu setMenuVisible:NO animated:YES];
[menu update];
return true;
}
return false;
}
- (void)copy:(id)sender {
UIPasteboard *board = [UIPasteboard generalPasteboard];
[board setString:self.text];
self.highlighted = NO;
[self resignFirstResponder];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if([self isFirstResponder]) {
//UIMenuController *menu = [UIMenuController sharedMenuController];
//[menu setMenuVisible:NO animated:YES];
//[menu update];
[self resignFirstResponder];
}
else if([self becomeFirstResponder]) {
//UIMenuController *menu = [UIMenuController sharedMenuController];
//[menu setTargetRect:self.bounds inView:self];
//[menu setMenuVisible:YES animated:YES];
}
}
- (void)setHighlighted:(BOOL)hl {
[super setHighlighted:hl];
[self setNeedsLayout];
}
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
if(self.highlighted) {
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetRGBFillColor(ctx, 0.3, 0.8, 1.0, 0.3);
CGContextAddRect(ctx, CGRectMake(0, 0, [self textRectForBounds:self.frame limitedToNumberOfLines:1].size.width, self.frame.size.height));
CGContextFillPath(ctx);
}
}
Any help appreciated!
Personally i would use a UITextView with option of editable set to NO.
otherwise, if you want more control, subclass your topmost fullscreen view (or window) that your UILabel is a child of.
override - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
and if the hit view is not your UILabel, then create a NSNotification which you handle in your UILabel subclass, and do the deselect.
BTW, You should ALWAYS handle touchesCanceled also!
I'm trying to get the following code work:
UIMenuController * menu = [UIMenuController sharedMenuController];
[menu setTargetRect: CGRectMake(100, 100, 100, 100) inView: self.view];
[menu setMenuVisible: YES animated: YES];
The menu instance is ready but it doesn't show - the width is always zero.
Or is there some sample code on this UIPasteboard/UIMenuController topic?
I was not able to get it working even when I read all of your answers. I'm presenting ready code that will work for everyone.
Let's say we have a controller class named Controller. You can simply paste the following code to this controller to have the menu working on its view:
- (void)loadView {
[super loadView];
UILongPressGestureRecognizer *gr = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress:)];
[self.view addGestureRecognizer:gr];
}
- (void) longPress:(UILongPressGestureRecognizer *) gestureRecognizer {
if ([gestureRecognizer state] == UIGestureRecognizerStateBegan) {
CGPoint location = [gestureRecognizer locationInView:[gestureRecognizer view]];
UIMenuController *menuController = [UIMenuController sharedMenuController];
UIMenuItem *resetMenuItem = [[UIMenuItem alloc] initWithTitle:#"Item" action:#selector(menuItemClicked:)];
NSAssert([self becomeFirstResponder], #"Sorry, UIMenuController will not work with %# since it cannot become first responder", self);
[menuController setMenuItems:[NSArray arrayWithObject:resetMenuItem]];
[menuController setTargetRect:CGRectMake(location.x, location.y, 0.0f, 0.0f) inView:[gestureRecognizer view]];
[menuController setMenuVisible:YES animated:YES];
}
}
- (void) copy:(id) sender {
// called when copy clicked in menu
}
- (void) menuItemClicked:(id) sender {
// called when Item clicked in menu
}
- (BOOL) canPerformAction:(SEL)selector withSender:(id) sender {
if (selector == #selector(menuItemClicked:) || selector == #selector(copy:)) {
return YES;
}
return NO;
}
- (BOOL) canBecomeFirstResponder {
return YES;
}
What has to be done in order for menu to work is that the firstResponder(in our case our controller - see line with [self becomeFirstResponder]) has to be able to become first responder (override method canBecomeFirstResponder cause default implementation returns NO) as well as - (BOOL) canPerformAction:(SEL)selector withSender:(id) sender which should return YES to any action that can be performed by firstResponder
If you're implementing a custom view and that view is supposed to be the responder (as opposed to some other view, like a UITextField), you need to override the canBecomeFirstResponder function in your view and return YES:
- (BOOL)canBecomeFirstResponder {
return YES;
}
then, when you're displaying the menu, you should do something like the following:
- (void)myMenuFunc {
if (![self becomeFirstResponder]) {
NSLog(#"couldn't become first responder");
return;
}
UIMenuController *theMenu = [UIMenuController sharedMenuController];
CGRect selectionRect = CGRectMake(0, 0, 0, 0);
[theMenu setTargetRect:selectionRect inView:self];
[theMenu setMenuVisible:YES animated:YES];
}
In case somebody still has problems: My menu used to work and some day stopped working miraculously. Everything else in my app still worked. Now I had removed the [window makeKeyAndVisible] method from application:didFinishLaunchingWithOptions: method, and while everything else still worked, this breaks UIMenuController!
Stupid error on my side, difficult to find the culprit...
to display UIMenuController one has to add following
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (action == #selector(cut:))
return NO;
else if (action == #selector(copy:))
return YES;
else if (action == #selector(paste:))
return NO;
else if (action == #selector(select:) || action == #selector(selectAll:))
return NO;
else
return [super canPerformAction:action withSender:sender];
}
I think Cam is right, need override both canPerformAction and canBecomeFirstResponder
- (BOOL) canPerformAction:(SEL)action withSender:(id)sender
{
if (action == #selector(doSomething:)) {
return YES;
}
return NO;
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
// MyView.h
#interface MyView : UIView {
IBOutlet UITextField * textField_;
}
#end
// MyView.m
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(#"show menu");
[textField_ becomeFirstResponder];
// [self.window becomeFirstResponder];
UIMenuController * menu = [UIMenuController sharedMenuController];
[menu setTargetRect: CGRectMake(0, 0, 100, 10) inView: self];
[menu setMenuVisible: YES animated: YES];
NSLog(#"menu width %f, visible %d", menu.menuFrame.size.width, menu.menuVisible);
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender{
return YES;
}
You need to add more code in canPerformAction:withSender: - it should check the pasteboard and your selection state. Apple's iPhone Application Programming Guide provide several code snippets.
Don't you have to add the UIMenuController* menu to the main or subview, E.G. self.view?
I think it's something like [self.view addSubView:menu.view]; Or am I missing the point of your question. You might also want to set the frame of the menu's view.
UIMenuController doesn't have a view. I just searched some code from apple's iPhone Application Programming Guide: Event Handling:
Listing 3-4 Displaying the editing menu
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *theTouch = [touches anyObject];
if ([theTouch tapCount] == 2 && [self becomeFirstResponder]) {
// selection management code goes here...
// bring up editing menu.
UIMenuController *theMenu = [UIMenuController sharedMenuController];
CGRect selectionRect = CGRectMake(currentSelection.x, currentSelection.y, SIDE, SIDE);
[theMenu setTargetRect:selectionRect inView:self];
[theMenu setMenuVisible:YES animated:YES];
}
}
I did it in the following way below. Just call the method that shows the menu after very short delay in init. I didn't want to call it from View Controller and also didn't find an event that indicates that my custom view appeared and i'm ready to show menu. So this way OK from my perspective. Delay can be less, but its up to you.
#implementation DTSignatureImageView
- (id)initWithImage:(UIImage *)image
{
self = [super initWithImage:image];
if(self){
self.contentMode = UIViewContentModeScaleAspectFit;
self.frame = CGRectMake(0, 0, image.size.width / 2.5, image.size.height / 2.5);
self.userInteractionEnabled = YES;
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(signatureDidPan:)];
[self addGestureRecognizer:pan];
[self becomeFirstResponder];
[self performSelector:#selector(showMenu) withObject:nil afterDelay:0.5];
}
return self;
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
- (void)showMenu
{
UIMenuController *menu = [UIMenuController sharedMenuController];
menu.menuItems = #[
[[UIMenuItem alloc] initWithTitle:#"Apply" action:#selector(applySignature)],
[[UIMenuItem alloc] initWithTitle:#"Update" action:#selector(updateSignature)],
[[UIMenuItem alloc] initWithTitle:#"Clear" action:#selector(delegateSignature)]];
[menu setTargetRect:self.bounds inView:self];
[menu setMenuVisible:YES animated:YES];
}
- (NSArray *)menuActions
{
static NSArray *actions = nil;
if (actions == nil){
actions = #[
NSStringFromSelector(#selector(applySignature)),
NSStringFromSelector(#selector(updateSignature)),
NSStringFromSelector(#selector(delegateSignature))];
}
return actions;
}
- (void) signatureDidPan: (UIPanGestureRecognizer *)gesture
{
switch (gesture.state) {
case UIGestureRecognizerStateBegan: {
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
break;
}
case UIGestureRecognizerStateEnded: {
[self becomeFirstResponder];
[self showMenu];
}
default:
break;
}
CGPoint point = [gesture locationInView:gesture.view.superview];
gesture.view.center = point;
}