在 UITextField 之外的任何地方关闭触摸键盘

Posted

技术标签:

【中文标题】在 UITextField 之外的任何地方关闭触摸键盘【英文标题】:Dismiss keyboard on touch anywhere outside UITextField 【发布时间】:2012-06-30 12:36:47 【问题描述】:

我正在开发一个 iPad 应用程序,该应用程序具有大量 UIViewControllersUITableViews(具有 accessoryViewsUITextFields 的单元格)等等。许多 UIViewControllers 出现在导航层次结构中。

UITextFields 出现在许多不同的地方,包括 UITableViewCell accessoryViews

我想设计一种有效的策略,以便在用户触摸当前正在编辑的UITextField 之外时关闭键盘。我搜索了键盘关闭技术,但尚未找到解释一般键盘关闭策略如何工作的答案。

例如,我喜欢这种方法,将以下代码添加到任何 ViewController 中:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 
    NSLog(@"* * * * * * * * *ViewControllerBase touchesBegan");

    [self.view endEditing:YES]; // dismiss the keyboard

    [super touchesBegan:touches withEvent:event];

...但是这种技术不能处理例如在显示的UITableView 内发生触摸的情况。所以,我需要添加一些代码来调用endEditingUITableView 被触摸等等等等。这意味着我的应用程序将大量撒上大量代码来关闭键盘当各种其他UIElements 是感动。

我想我可以尝试找出需要拦截触摸和关闭键盘的所有不同位置,但在我看来,可能有更好的设计模式来处理 ios 键盘关闭事件。

任何人都可以分享他们在这方面的经验,并推荐一种在整个应用程序中通用处理键盘关闭的特定技术吗?

非常感谢

【问题讨论】:

【参考方案1】:

您的视图层次结构位于UIWindow 中。 UIWindow 负责在其sendEvent: 方法中将触摸事件转发到正确的视图。让我们创建一个UIWindow 的子类来覆盖sendEvent:

@interface MyWindow : UIWindow
@end

窗口将需要对当前第一响应者的引用(如果有的话)。您可能还决定使用UITextView,因此我们将观察来自文本字段和文本视图的通知。

@implementation MyWindow 
    UIView *currentFirstResponder_;


- (void)startObservingFirstResponder 
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(observeBeginEditing:) name:UITextFieldTextDidBeginEditingNotification object:nil];
    [center addObserver:self selector:@selector(observeEndEditing:) name:UITextFieldTextDidEndEditingNotification object:nil];
    [center addObserver:self selector:@selector(observeBeginEditing:) name:UITextViewTextDidBeginEditingNotification object:nil];
    [center addObserver:self selector:@selector(observeEndEditing:) name:UITextViewTextDidEndEditingNotification object:nil];


- (void)stopObservingFirstResponder 
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self name:UITextFieldTextDidBeginEditingNotification object:nil];
    [center removeObserver:self name:UITextFieldTextDidEndEditingNotification object:nil];
    [center removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil];
    [center removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil];


- (void)observeBeginEditing:(NSNotification *)note 
    currentFirstResponder_ = note.object;


- (void)observeEndEditing:(NSNotification *)note 
    if (currentFirstResponder_ == note.object) 
        currentFirstResponder_ = nil;
    

窗口将在初始化时开始观察通知,并在释放时停止:

- (id)initWithCoder:(NSCoder *)aDecoder 
    if ((self = [super initWithCoder:aDecoder])) 
        [self commonInit];
    
    return self;


- (id)initWithFrame:(CGRect)frame 
    if ((self = [super initWithFrame:frame])) 
        [self commonInit];
    
    return self;


- (void)commonInit 
    [self startObservingFirstResponder];


- (void)dealloc 
    [self stopObservingFirstResponder];

我们会覆盖sendEvent:根据事件“调整”第一响应者,然后调用super的sendEvent:正常发送事件。

- (void)sendEvent:(UIEvent *)event 
    [self adjustFirstResponderForEvent:event];
    [super sendEvent:event];

如果没有第一响应者,我们不需要对第一响应者做任何事情。如果有第一响应者,并且它包含一个触摸,我们不想强迫它辞职。 (记住,可以同时有多个触摸!)如果有第一响应者,并且新的触摸出现在另一个可以成为第一响应者的视图中,系统会自动正确处理,所以我们也想忽略这种情况。但是,如果有一个第一响应者,并且它不包含任何触摸,并且一个新的触摸出现在 不能成为第一响应者的视图中,我们想让第一响应者辞职。

- (void)adjustFirstResponderForEvent:(UIEvent *)event 
    if (currentFirstResponder_
        && ![self eventContainsTouchInFirstResponder:event]
        && [self eventContainsNewTouchInNonresponder:event]) 
        [currentFirstResponder_ resignFirstResponder];
    

在第一响应者中报告事件是否包含触摸很容易:

- (BOOL)eventContainsTouchInFirstResponder:(UIEvent *)event 
    for (UITouch *touch in [event touchesForWindow:self]) 
        if (touch.view == currentFirstResponder_)
            return YES;
    
    return NO;

报告事件是否包含无法成为第一响应者的视图中的新触摸几乎同样简单:

- (BOOL)eventContainsNewTouchInNonresponder:(UIEvent *)event 
    for (UITouch *touch in [event touchesForWindow:self]) 
        if (touch.phase == UITouchPhaseBegan && ![touch.view canBecomeFirstResponder])
            return YES;
    
    return NO;


@end

一旦你实现了这个类,你需要改变你的应用来使用它而不是UIWindow

如果您在application:didFinishLaunchingWithOptions: 中创建UIWindow,则需要在AppDelegate.m 的顶部添加#import "MyWindow.h",然后更改application:didFinishLaunchingWithOptions: 以创建MyWindow 而不是UIWindow .

如果您在 nib 中创建 UIWindow,则需要在 nib 中将窗口的自定义类设置为 MyWindow

【讨论】:

非常感谢您提供非常全面的答案。 有什么方法可以拦截一个视图控制器内的事件,以防止上面 MyWindow 中设置的一般行为?只有在一个视图控制器上,我需要让我的键盘一直出现。 你可以沿着响应者链向上走,从currentFirstResponder_开始,询问每个响应者是否想要阻止特殊处理。 非常好的解决方案!谢谢! 我注意到取消(删除文本)按钮在实施此解决方案后停止工作。如果您希望恢复此行为,请在 eventContainsTouchInFirstResponder 中编辑此行:if (touch.view == currentFirstResponder_ || touch.view.superview == currentFirstResponder_) return YES;【参考方案2】:

这是一种更简单有效的处理方法。这适用于视图控制器中的任何 UITextField。你甚至可以将它添加到你的基本视图控制器(如果你有的话),它会像一个魅力一样工作。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 

    UITouch *touch = [[event allTouches] anyObject];

    if (![[touch view] isKindOfClass:[UITextField class]]) 
        [self.view endEditing:YES];
    
    [super touchesBegan:touches withEvent:event];

【讨论】:

很好的答案。很有帮助。谢谢! 这仅适用于视图。如果您在该视图上有其他控件,例如 textViews、按钮、segmentedControls 等,它将无法正常工作。 @RubberDuck - 不知道你的意思是什么,它只适用于view,你能详细说明一下,因为我在我的所有UIViewControllers 中都按预期工作吗? 我在视图控制器中添加了这个,但是当我点击外部时没有任何反应,你能详细说明如何实现这个吗?似乎是迄今为止最简单的答案。 当我点击表格视图的任何单元格时,我的 UITableViewController 子类中根本没有看到这个调用。运行 iOS 8.2。【参考方案3】:

我一般用下面的

第一:

在您的实现文件中包含以下扩展名,findFirstResponder 将帮助您找到第一个响应者

@implementation UIView (FindFirstResponder)
- (UIView*)findFirstResponder

    if (self.isFirstResponder) 
        return self;     
    
    for (UIView *subView in self.subviews) 
        UIView *responder = [subView findFirstResponder];
        if (responder)
            return responder;
    

    return nil;

@end

然后在您的视图控制器viewDidLoad 中添加以下内容

- (void)viewDidLoad

    [super viewDidLoad];
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(keyboardDidShow:) 
                   name:UIKeyboardDidShowNotification 
                 object:nil];

    [center addObserver:self selector:@selector(keyboardDidHide:) 
                   name:UIKeyboardWillHideNotification 
                 object:nil];
    // Do any additional setup after loading the view, typically from a nib.

通知功能会是这样的

- (void) keyboardDidShow:(NSNotification*)notification
    
    UIButton *button = [[UIButton alloc] init];

    CGRect rect = self.view.bounds;


    button.frame = rect;
    button.backgroundColor = [UIColor blackColor];
    button.tag = 111;
    UIView *currentResponer = [self.view findFirstResponder];
    [button addTarget:currentResponer action:@selector(resignFirstResponder) forControlEvents:UIControlEventTouchUpInside];

    [self.view insertSubview:button belowSubview:currentResponer];


- (void) keyboardDidHide:(NSNotification*)notification

    [[self.view viewWithTag:111] removeFromSuperview];

当键盘显示时,我在当前第一响应者下方添加UIButton,此按钮操作将隐藏键盘,

这里的一个限制是 UITextField 必须在 self.view 中,但是您可以通过一些修改来适应这种技术以满足您的需要,希望它对您有所帮助

【讨论】:

【参考方案4】:

我真的很喜欢 Omar 的技术 - 它工作得很好 - 但我在我的案例中添加了几行。

- (void)keyboardDidShow:(NSNotification*)notification

    UIButton *button = [[UIButton alloc] init];

    CGRect rect = self.view.bounds;

    button.frame = rect;
    button.backgroundColor = [UIColor blackColor];

    [button setAlpha:0.5f];
    button.tag = 111;
    UIView *currentResponder = [self.view findFirstResponder];

    [self.view bringSubviewToFront:currentResponder];

    if (currentResponder == _descriptionTextView) [_descriptionTextView setTextColor:[UIColor whiteColor]];

    [button addTarget:currentResponder action:@selector(resignFirstResponder) forControlEvents:UIControlEventTouchUpInside];

    [self.view insertSubview:button belowSubview:currentResponder];

我在他的解决方案中添加了三行(见上文):

    将按钮的 alpha 设置为 0.6,这样我就可以看到我的一些 它背后的视图控制器。 将 currentResponder 置于最前面, 这样我的其他视图都不会出现在按钮前面。 切换我的 UITextView 的颜色,使文本更清晰(我的 UITextView 具有清晰的背景,因此没有显示文本 清楚)。

无论如何,只有微小的变化,但希望这些有所帮助。谢谢奥马尔。

【讨论】:

【参考方案5】:

为了辞去当前的第一响应者,我尝试使用 endEditing,但在设置第一响应者之后遇到了一些问题。然后我使用了一段时间的递归方法在 UIView 类别方法中找到了第一响应者:

- (UIView *)getFirstResponder 

    if (self.isFirstResponder) 
        return self;
    

    for (UIView *subView in self.subviews) 
        UIView *firstResponder = [subView getFirstResponder];

        if (firstResponder != nil) 
            return firstResponder;
        
    

    return nil;

一旦返回响应者,我就可以调用 resignFirstResponder 了。

不过,就在今天,我的一个工作伙伴在这里向我展示了一个更好的方法:http://overooped.com/post/28510603744/i-had-completely-forgotten-about-nil-targeted

就这样称呼吧:

[[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil];

真棒一个班轮!

【讨论】:

【参考方案6】:

在 Swift 3 中

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) 

    self.view.endEditing(true)

【讨论】:

【参考方案7】:

可能是我不明白你想要什么,但- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch 方法可能会帮助你关闭键盘

【讨论】:

【参考方案8】:

为文本字段控件添加“textFieldDidChange”通知方法。

[textField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingDidEnd];

为我工作

【讨论】:

以上是关于在 UITextField 之外的任何地方关闭触摸键盘的主要内容,如果未能解决你的问题,请参考以下文章

iOS - 在 UITextField 之外触摸时关闭键盘

包含 UITextfield 的子视图未注册 Tapgesture

Navigation Drawer 一段时间后,触摸屏幕的任何地方都保持关闭

检测呈现视图之外的触摸以将其关闭

当用户在 UITextField 之外点击时关闭键盘 [重复]

UITextField 是不是消耗触摸事件?