无法将搜索栏设置为第一响应者。

58

我有一个搜索栏(SearchBar),我将其设置在一个表视图控制器(TableViewController)中。我参考了这个类似的问题UISearchBar cannot become first responder after UITableView did re-appear,但是我仍然无法将其设置为第一响应者。 .h文件中:

@property (strong, nonatomic) UISearchController *searchController;
@property (strong, nonatomic) IBOutlet UISearchBar *searchBar;

在viewDidLoad方法中:

self.searchController = [[UISearchController alloc]initWithSearchResultsController:nil];
self.searchController.searchResultsUpdater = self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.searchController.hidesNavigationBarDuringPresentation = NO;

self.searchController.searchBar.frame = CGRectMake(self.searchController.searchBar.frame.origin.x, self.searchController.searchBar.frame.origin.y, self.searchController.searchBar.frame.size.width, 44.0);
self.tableView.tableHeaderView = self.searchController.searchBar;

并且在viewDidAppear方法中:

-(void)viewDidAppear:(BOOL)animated {
  [self.searchController setActive:YES];
  [self.searchController.searchBar becomeFirstResponder];
  [super viewDidAppear:animated];
}

当我切换到搜索视图时,搜索栏会发生动画,但键盘没有弹出。


如果您使用dispatch_after()稍微延迟becomeFirstResponder方法会发生什么? - diederikh
你为什么在 super viewDidAppear 之前调用它,有什么原因吗? - CrimsonChris
我尝试过放在前面和后面,但都没有成功。搜索栏是通过编程创建的,我可能错过了一些明显的连接吗? - user3411663
22个回答

61
我也注意到了这个问题。看起来发生的是在搜索控制器仍在“加载”状态时调用了becomeFirstResponder。如果您注释掉becomeFirstResponder,则会发现没有任何区别。因此,我们需要一种方法在搜索控制器“完成”加载后调用becomeFirstResponder。
当我查看各种委托方法时,我注意到有一个委托方法:
- (void)didPresentSearchController:(UISearchController *)searchController

在searchController被呈现后立即调用此方法。 然后我调用becomeFirstResponder

- (void)didPresentSearchController:(UISearchController *)searchController
{
    [searchController.searchBar becomeFirstResponder];
}

这个修复程序解决了问题。您会注意到当searchController被加载时,搜索栏现在具有焦点。


8
在iOS8中,这对我不起作用。只有当我手动触摸SearchBar的文本字段时,才会调用didPresentSearchController - Besi
8
我知道你犯了什么错误,你需要打开/激活searchViewController,比如在你的viedDidLoad中加入这行代码:[self.searchController setActive:TRUE];。请注意,我的翻译不包括解释和其他内容。 - edwardmp
6
请确保将 becomeFirstResponder 的调用也放在主线程上。因为在某些 iOS 版本中,didPresentSearchController 方法会在后台线程中被调用! - simonthumper
5
必须在viewDidAppear方法中添加 [self.searchController setActive:TRUE];,如果放在viewDidLoad中,它不起作用。 - BobGao
6
iOS 9.3无法正常工作。mislovr的回答或使用dispatch_after是唯一的解决方案。 - skensell
显示剩余6条评论

48

使用 (void)didPresentSearchController:(UISearchController *)searchController 这个代理方法并不能解决问题,因为这个方法只有在用户点击搜索栏时才会被调用...

然而,下面这个解决方案可以解决问题:

- (void) viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [self performSelector:@selector(showKeyboard) withObject:nil afterDelay:0.1];
}

- (void) showKeyboard
{
    [self.searchController.searchBar becomeFirstResponder];
}

Swift 3

delay(0.1) { self.searchController.searchBar.becomeFirstResponder() }

func delay(_ delay: Double, closure: @escaping ()->()) {
    let when = DispatchTime.now() + delay
    DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}

2
更简单地说:[self.searchController.searchBar performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0.1]; - Brainware
这对我有用。仍然是一个有效的答案。 - Dipak Mishra

19

好的,我找到了一个对我来说完美有效的解决方案。

在调用[self.searchController.searchBar becomeFirstResponder];之前,请不要调用[self.searchController setActive:YES];

更好的是,根本不要调用[self.searchController setActive:YES];

只调用[self.searchController.searchBar becomeFirstResponder];,键盘就会像应该的那样立即弹出,没有任何延迟。

这似乎有点像一个bug,很多人都确认了。例如,请查看这里:当使用becomeFirstResponder分配焦点到UISearchController的UISearchBar时,键盘没有出现


4
对我来说它不起作用。你在哪里调用 [self.searchController.searchBar becomeFirstResponder]; - Adam Studenic

15

Swift 4,iOS 11

对我而言有效

// 1.
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    resultSearchController.isActive = true
}

// 2. ->> UISearchControllerDelegate
func didPresentSearchController(_ searchController: UISearchController) {

    DispatchQueue.main.async {
        searchController.searchBar.becomeFirstResponder()
    }
}

对我来说可以工作,注意我需要在viewWillAppear中设置self.definesPresentationContext = true,以便它正确地运行。 - Alain Stulz
无法工作。请查看https://dev59.com/XV0Z5IYBdhLWcg3wlRQV。 - tanmoy

14

searchController.searchBar.becomeFirstResponder()方法必须在主线程中调用,并且必须在searchController.active = true之后在viewDidLoad方法中调用。以下是完整的解决方案。该方案适用于iOS 9.3。

override func viewDidAppear(animated: Bool) {
  super.viewDidAppear(animated)
  searchController.active = true
  Async.main {
    self.searchController.searchBar.becomeFirstResponder()
  }
}

2
首先,将 searchController.active 设为 true 对我有用。至于为什么这里需要在视图的生命周期方法中调用 Async.main 呢?这不是已经保证在主线程上运行了吗? - kyleturner
2
UISearchController目前似乎有一些未完成的工作(好像他们过早地调用了它们的委托方法),而Async.main只是简单地安排becomeFirstResponder()在将来的迭代中发生,绕过了这个错误。 - Kieran Harper
你现在只是碰巧通过将它分派到主队列来使其在搜索控制器完成呈现后被执行。正确的解决方案是从didPresentSearchController代理方法中调用becomeFirstResponder - Joel

8

与其他答案非常相似,但我必须在 ViewDidAppear 中访问主队列。 只有在 View 出现后才能对 SearchBarController 进行操作,并且只能在 UI 的主队列中完成:

searchController.active = true  // doubtful whether this is needed

        dispatch_async(dispatch_get_main_queue(), {
            self.searchController.searchBar.becomeFirstResponder()
        });

3
没有设置“active”变量,这使得它对我起作用了。 - Raymond26
4
viewDidAppear方法总是在主队列上被调用。实际上,这段代码将becomeFirstResponder()方法的执行安排到未来的主队列迭代中,以便让UISearchController在此时完成其尚未完成的工作。 - Kieran Harper

7

简单易懂的 Swift3 变量:

override func viewDidLoad() {
    super.viewDidLoad()
    navigationItem.titleView = mySearchController.searchBar
    mySearchController.searchResultsUpdater = self
    mySearchController.delegate = self

}

override func viewDidAppear(_ animated: Bool) {
    DispatchQueue.main.async {
        self.mySearchController.isActive = true
    }
}

func presentSearchController(_ searchController: UISearchController) {
    mySearchController.searchBar.becomeFirstResponder()
}

它可以工作;-)


为什么在 viewDidAppear 中调用 DispatchQueue.main.async?因为除了主线程,永远不会有任何情况调用 viewDidAppear,所以根本不需要这样做。 - edwardmp
1
@edwardmp 这种情况可能会发生,但是这个调度解决了我的问题。想给vitya五响礼炮。 - NaXir

6

Swift 5 解决方案:

override func viewDidLoad() {
        super.viewDidLoad()
        definesPresentationContext = true
        searchController.delegate = self
    }

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    searchController.isActive = true
}

extension ViewController: UISearchControllerDelegate {
 func didPresentSearchController(_ searchController: UISearchController) {
      DispatchQueue.main.async {[weak self] in
          self?.searchController.searchBar.becomeFirstResponder()
       }
  }
}

在iOS 11、iOS 12和iOS 13上运行良好。 - Benoit Deldicque
是的,这个方法可以工作,可惜动画需要一段时间才能完成并且键盘弹出。尝试过viewWillAppear但没有起作用。 - Nico S.

6

Xcode 11.4,Swift 5.2

如果你只想让SearchBar出现,但不激活TextField和键盘。则无需将其调度到主线程。

enter image description here

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    searchController.isActive = true
}

如果您希望使用键盘激活 TextField,则确实需要在主线程上调用它。无需使SearchController处于活动状态,这会自动发生。

输入图像描述

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    DispatchQueue.main.async {
        self.searchController.searchBar.becomeFirstResponder()
    }
}

这可能取决于您如何配置您的SearchController。这是我如何配置我的:
// Defined in class
let searchController = UISearchController(searchResultsController: nil)

// Called in viewDidLoad
navigationItem.searchController = searchController
searchController.searchResultsUpdater = self
searchController.searchBar.scopeButtonTitles = ["A", "B"]
searchController.searchBar.delegate = self          
searchController.obscuresBackgroundDuringPresentation = false

这实际上是最新的答案。我想要第二个选项,在iOS 13.6中它起作用了。 - alobaili
@rbaldwin,老兄。 - Sreekanth G
这个可以运行,但是有一个问题。我在第一个视图控制器中推出第二个视图控制器后,在第二个视图控制器中有一个搜索栏。它在第一次推出时可以正常工作,但是当我点击返回并再次回到这个屏幕时,会显示错误“尝试呈现已经呈现的UISearchController”。为了解决这个问题,请在viewDidLoad中调用以下代码: definesPresentationContext = true - Davuth
这是在iOS 14上对我有效的唯一解决方案。谢谢! - VoodooBoot

5

替代方案

本答案涉及到与此问题相关的一些问题,并解释了我在隔离和确定问题特定原因时使用的调试逻辑。在此过程中,我分享了其他可能在其他情况下起作用的可能性,并解释了为什么这在我的情况下有效。


简而言之,看一下self.definesPresentationContext = true、isBeingDismissed、isBeingPresented、canBecomeFirstResponder以及delegate的分配,包括SearchController、Searchbar、SearchResultsUpdater及其委托方法。
(self.definesPresentationContext解决方案来源-请参见SO答案
需要记住的一件事是搜索栏的呈现方式的上下文。它可以嵌入到工具栏、导航栏、另一个UIView中,也可以作为输入或输入辅助视图。这些方式都会对搜索栏的内部动画和时间产生一定影响。
我尝试了所有提出的解决方案,但直到重新考虑如何使用搜索栏时,才发现没有一个解决方案可行。在我的情况下,我从已经有初始搜索控制器的控制器A中推送了一个带有搜索控制器的控制器B。当进行下拉刷新时,我通过编程将每个搜索控制器嵌入到我的导航项的titleView中。
这些建议都不适用于我的用例,因为当我想要将搜索栏插入并显示到navigationItem时,视图已经完全加载了。而且,逻辑似乎很混乱,因为视图控制器生命周期方法应该已经在主线程上运行。将演示延迟也似乎干扰了系统使用的显示和动画的内部操作。
我发现当我从controllerA推送视图控制器时,调用我的函数来切换插入是有效的,但是当我从controllerB推送时,键盘无法正确显示。两种情况之间的区别是,controllerA是静态tableview控制器,而controllerB具有我添加了自己的搜索控制器的tableview控制器。
通过使用多个断点并检查不同时间点的searchController和searchbar的状态,我能够确定searchController.canBecomeFirstResponder返回false。我还发现需要将SearchResultsUpdater设置为self,并在searchController和searchBar上设置委托。

我最终注意到,在控制器A上设置self.definesPresentationContext = true时,当我将控制器B推到导航堆栈上时无法显示键盘。我的解决方案是将self.definesPresentationContext = true移到控制器A的viewDidAppear中,并在控制器A的prepare(for:sender:)方法中将其更改为self.definesPresentationContext = false,当目标是控制器B时。这解决了我的情况下的键盘显示问题。

关于动画的说明 ~ 我发现在将事物分配给navigationItem或navigationBar时,系统具有一些内置的时间和默认动画。我避免添加自定义动画、在moveToParent方法中编写代码或延迟演示,因为在许多情况下会出现意想不到的行为。

为什么这个解决方案有效

苹果公司文档definesPresentationContext进行了说明,指出了默认行为并注释了一些情况,其中此上下文调整了分配给管理键盘外观的控制器的行为。在我的情况下,控制器A被指定来管理演示,而不是控制器B,因此我通过调整这个值来改变控制器的行为:

使用currentContext或overCurrentContext样式来呈现视图控制器时,此属性控制实际上被新内容覆盖的现有视图控制器。当基于上下文的呈现发生时,UIKit从呈现视图控制器开始并向上遍历视图控制器层次结构。如果找到一个值为true的视图控制器,则要求该视图控制器呈现新视图控制器。如果没有视图控制器定义呈现上下文,则UIKit要求窗口根视图控制器处理呈现。此属性的默认值为false。一些系统提供的视图控制器,例如UINavigationController,将默认值更改为true。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接