Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIMenuController hides the keyboard

I currently have an application which is for chatting. I used a UItextField for input box and bubbles for display messages, some thing like the system SMS. I want to enable copy paste on the message bubbles (labels). The problem is, when I want to show the UIMenuController, the label which i need to copy from need to become first responder. If the keyboard is currently displayed, when the label become first responder, the textfield will lost focus, thus the keyboard will be hide automatically. this cause an UI scroll and feels not good. Is there anyway that i can keep the keyboard shown even when i need to show the menu?

enter image description here

enter image description here

like image 223
Anurag Kabra Avatar asked Nov 28 '12 09:11

Anurag Kabra


3 Answers

For those who still looking for answer here is code (main idea belongs to neon1, see linked question).

The idea is following: if a responder doesn't know how to handle given action, it propogates it to the next responder in chain. Until now we have two candidates for first responders:

  1. Cell
  2. TextField

Each of them have separate chain of responders (in fact, no, they do have common ancestor, so their chains have something in common, but we cannot use it):

UITextField <- UIView <- ... <- UIWindow <- UIApplication
UITableViewCell <- UIView <- ... <- UIWindow <- UIApplication

So we would like to have following chain of reponders:

UITextField <- UITableViewCell <- ..... <- UIWindow <- UIApplication

We need to subclass UITextField (code is taken from here):

CustomResponderTextView.h

@interface CustomResponderTextView : UITextView
@property (nonatomic, weak) UIResponder *overrideNextResponder;
@end

CustomResponderTextView.m

@implementation CustomResponderTextView

@synthesize overrideNextResponder;

- (UIResponder *)nextResponder {
    if (overrideNextResponder != nil)
        return overrideNextResponder;
    else
        return [super nextResponder];
}

@end

This code is very simple: it returns real responder in case we haven't set any custom next responder, otherwise returns our custom responder.

Now we can set new responder in our code (my example adds custom actions):

CustomCell.m

@implementation CustomCell
- (BOOL) canBecomeFirstResponder {
    return YES;
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
    return (action == @selector(copyMessage:) || action == @selector(deleteMessage:));
}
@end

- (void) copyMessage:(id)sender {
   // copy logic here
}

- (void) deleteMessage:(id)sender {
   // delete logic here
}

Controller

- (void) viewDidLoad {
    ...
    UIMenuItem *copyItem = [[UIMenuItem alloc] initWithTitle:@"Custom copy" action:@selector(copyMessage:)];
    UIMenuItem *deleteItem = [[UIMenuItem alloc] initWithTitle:@"Custom delete" action:@selector(deleteMessage:)];
    UIMenuController *menu = [UIMenuController sharedMenuController];
    [menu setMenuItems:@[copyItem, deleteItem]];
    ...
}

- (void) longCellTap {
    // cell is UITableViewCell, that has received tap
    if ([self.textField isFirstResponder]) {
        self.messageTextView.overrideNextResponder = cell;
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(menuDidHide:) name:UIMenuControllerDidHideMenuNotification object:nil];
    } else {
        [cell becomeFirstResponder];
    }
}

- (void)menuDidHide:(NSNotification*)notification {
    self.messageTextView.overrideNextResponder = nil;
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIMenuControllerDidHideMenuNotification object:nil];
}

Last step is making first responder (in our case text field) propogate copyMessage: and deleteMessage: actions to next responder (cell in our case). As we know iOs sends canPerformAction:withSender: to know, if given responder can handle the action.

We need to modify CustomResponderTextView.m and add the following function:

CustomResponderTextView.m

...
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
    if (overrideNextResponder != nil)
        return NO;
    else
        return [super canPerformAction:action withSender:sender];
}
...

In case we've set our custom next responder we send all actions to it (you can modify this part, if you need some actions on textField), otherwise we ask our supertype if it can handles it.

like image 142
Nikita Took Avatar answered Oct 31 '22 05:10

Nikita Took


You can try to subclass your uitextfield and override the firstresponder. Check in your long press gesture handler if the uitextfield is the first responder and override the nextresponder.

like image 2
batman Avatar answered Oct 31 '22 06:10

batman


enter image description here

Just did it in Swift via Nikita Took's solution.

I have a chat screen where there is a Text Field for text Input and Labels for messages (their display). When you tap on a message label, MENU (copy/paste/...) should appear, but the keyboard must stay open if already.

I subclassed the input text field:

import UIKit

class TxtInputField: UITextField {

weak var overrideNextResponder: UIResponder?

override func nextResponder() -> UIResponder? {
  if overrideNextResponder != nil {
    return overrideNextResponder
  } else {
    return super.nextResponder()
  }
}

override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
  if overrideNextResponder != nil {
    return false
  } else {
    return super.canPerformAction(action, withSender: sender)
  }
 }
}

Then in my custom message label (subclass of UILabel but it can be a View Controller in your case) which has logic to start UIMenuController, I added after

if recognizer.state == UIGestureRecognizerState.Began { ... 

the following chunk

if let activeTxtField = getMessageThreadInputSMSField() {
  if activeTxtField.isFirstResponder() {
    activeTxtField.overrideNextResponder = self
  } else {
    self.becomeFirstResponder()
  }
} else {
  self.becomeFirstResponder()
}

When user taps outside of UIMenuController

func willHideEditMenu() {
    if let activeTxtField = getMessageThreadInputSMSField() {
      activeTxtField.overrideNextResponder = nil
   }
    NSNotificationCenter.defaultCenter().removeObserver(self, name: UIMenuControllerWillHideMenuNotification, object: nil)
  }

You have to get the reference to the activeTxtField object. I did it iterating the Navigation stack, getting my View Controller which holds the desired text field and then using it.

Just in case you need it, here is the snippet for that part as well.

var activeTxtField = CutomTxtInputField()
  for vc in navigationController?.viewControllers {
    if vc is CustomMessageThreadVC {
     let msgVC = vc as! CustomMessageThreadVC         
     activeTxtField = msgVC.textBubble
   }
}
like image 2
Pavle Mijatovic Avatar answered Oct 31 '22 06:10

Pavle Mijatovic