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?
问题:
回答1:
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.
回答2:
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:
- Cell
- 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.
回答3:
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
}
}