无法将 searchBar 设置为 firstResponder

Cannot set searchBar as firstResponder

我在 tableviewcontroller 中设置了一个 searchBar。我已经引用了这个类似的问题 UISearchBar cannot become first responder after UITableView did re-appear 但我仍然无法将其设置为第一响应者。 在 .h 文件中:

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

在视图中加载:

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];
}

当我切换到视图时,searchBar 动画化,但没有键盘出现。

我也注意到了这个问题。似乎发生的是当 searchController 仍然是 'loading' 时调用 becomeFirstResponder。如果您注释掉 becomeFirstResponder,您会发现没有区别。所以我们需要一种方法来在 searchController 'done' loading.

之后调用 becomeFirstResponder

当我查看各种委托方法时,我注意到有一个委托方法:

- (void)didPresentSearchController:(UISearchController *)searchController

在显示 searchController 之后立即调用此方法。然后我打电话给 becomeFirstResponder:

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

这解决了问题。您会注意到,当加载 searchController 时,搜索栏现在有了焦点。

解决方案 - (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)
}

我认为可能有更简洁的解决方案。我发现键盘在出现时有点 'blipping' 向上然后又向下,并且在 didPresentSearchController: 中调用 becomeFirstResponder 是有效的,但是键盘来晚了,动画是有点古怪。

用检查演示包装我的重新加载数据方法让每个人都很高兴:

- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
    if (!searchController.isBeingPresented && !searchController.isBeingDismissed) {
        [self.collectionView reloadData];
    }
}

我通过在 resignFirstResponder 中设置断点找到了这个。 resignFirstResponderreloadData 调用,reloadData 又被 updateSearchResultsForSearchController: 调用。结果是 updateSearchResultsForSearchController: 在搜索控制器的呈现期间被调用。如果你在这段时间内弄乱了 UISearchBar 所在的视图层次结构,搜索控制器就会变得乏味。我的猜测是 reloadData 调用导致 UICollectionReusableView header 视图出来并返回到视图层次结构并迫使 UISearchBar 子视图退出第一响应者。

我看到的另一个症状是搜索词在取消时没有重置到搜索栏的中间,这导致它在以后的点击中无法正确显示。

混合@edwardmp 和@mislovr 答案的解决方案对我来说有点奏效(键盘弹出稍有延迟):

- (void)didPresentSearchController:(UISearchController *)searchController {
    [self performSelector:@selector(showKeyboard) withObject:nil afterDelay:0.001];
}

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

好吧,我找到了真正适合我的解决方案。

在调用 [self.searchController.searchBar becomeFirstResponder];

之前不要调用 [self.searchController setActive:YES];

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

只调用 [self.searchController.searchBar becomeFirstResponder]; 键盘就会弹出,没有任何延迟。

好像有点bug,很多人都在确认。例如,检查此处:When assigning focus via becomeFirstResponder to UISearchController's UISearchBar, the keyboard does not appear

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

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

与其他答案非常相似,但我不得不访问ViewDidAppear中的主队列。 在 View 出现之前无法对 SearchBarController 采取行动,然后只能在 UI:

的主队列中这样做
searchController.active = true  // doubtful whether this is needed

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

我为此苦苦挣扎了一段时间,但通过以下方式使其正常工作:

  • 正在初始化 viewDidLoad()
  • 中的 searchController
  • viewDidAppear() 中设置 active = true
    • 这会触发 UISearchControllerDelegate 扩展中的 didPresentSearchController()
  • didPresentSearchController()
  • 中设置 searchBar.becomeFirstResponder()

这是完整的示例,它使用 Google 地图自动完成功能。

class myViewController: UIViewController {

    // MARK: Variables

    var resultsViewController: GMSAutocompleteResultsViewController?
    var searchController: UISearchController?
    var resultView: UITextView?

    // MARK: Outlets

    @IBOutlet var myView: UIView!

    // MARK: View Methods

    override func viewDidLoad() {
        super.viewDidLoad()

        resultsViewController = GMSAutocompleteResultsViewController()
        resultsViewController?.delegate = self

        searchController = UISearchController(searchResultsController: resultsViewController)
        searchController?.delegate = self
        searchController?.searchResultsUpdater = resultsViewController

        searchController?.searchBar.prompt = "Search for a Place"
        searchController?.searchBar.placeholder = "place name"
        searchController?.searchBar.text = ""
        searchController?.searchBar.sizeToFit()

        searchController?.searchBar.returnKeyType = .Next
        searchController?.searchBar.setShowsCancelButton(true, animated: false)

        myView.addSubview((searchController?.searchBar)!)
    }

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(true)
        searchController?.active = true
    }

    // MARK: GMSAutocompleteResultsViewControllerDelegate Extension

    extension myViewController: GMSAutocompleteResultsViewControllerDelegate {

        func resultsController(resultsController: GMSAutocompleteResultsViewController,
                       didAutocompleteWithPlace place: GMSPlace) {
            searchController?.active = false
            // Do something with the selected place.
            print("Place name: ", place.name)
            print("Place address: ", place.formattedAddress)
            print("Place attributions: ", place.attributions)
        }

        func resultsController(resultsController: GMSAutocompleteResultsViewController,
                       didFailAutocompleteWithError error: NSError){
            // TODO: handle the error.
            print("Error: ", error.description)
        }

        // Turn the network activity indicator on and off again.
        func didRequestAutocompletePredictionsForResultsController(resultsController: GMSAutocompleteResultsViewController) {
            UIApplication.sharedApplication().networkActivityIndicatorVisible = true
        }

        func didUpdateAutocompletePredictionsForResultsController(resultsController: GMSAutocompleteResultsViewController) {
            UIApplication.sharedApplication().networkActivityIndicatorVisible = false
        }
    }

    extension myViewController: UISearchControllerDelegate {

        func didPresentSearchController(searchController: UISearchController) {
            self.searchController?.searchBar.becomeFirstResponder()
        }
    }
}

简单 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 时会有很大的滞后。在viewDidLoad中异步使用becomeFirstResponder会更好(用iOS10,Swift3测试):

override func viewDidLoad() {
    super.viewDidLoad()

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

替代方法

This answer looks at a number of issues related to this occurrence and explains the debugging logic used to isolate and determine the specific cause of the problem in my case. Along the way, I share other possibilities that might have worked in other situations and explain why this works in my situation.


tldr; Look at the self.definesPresentationContext = true, the isBeingDismissed, isBeingPresented, canBecomeFirstResponder, and delegate assignment of the SearchController, the Searchbar, SearchResultsUpdater and their delegate methods.

(self.definesPresentationContext 解决方案的来源 - See SO answer)

要记住的一件事是如何呈现 SearchBar 的上下文。嵌入工具栏、导航栏、另一个 UIView,作为输入或输入附件视图。我发现所有这些都会对搜索栏的显示或关闭时的时间和内部动画产生一些影响。

我已经尝试了所提供的所有解决方案并且 none 这些解决方案都有效,直到我重新考虑我是如何尝试使用 searchBar 的。在我的例子中,我正在推动一个控制器 (B) 和一个来自控制器 (A) 的搜索控制器,它上面已经有一个初始搜索控制器.在进行拉动刷新时,我以编程方式将每个搜索控制器嵌入到导航项的 titleView 中。

建议将 searchbar.becomeFirstResponder() 添加到生命周期的答案对我的用例没有意义,因为在我想将搜索栏插入并显示到导航项时视图已完全加载。逻辑似乎也很混乱,因为视图控制器生命周期方法应该已经在主线程上运行了。在演示中添加延迟似乎也干扰了系统使用的显示和动画的内部操作。

我发现当我从 controllerA 推送视图控制器时调用我的函数来切换插入有效,但是当从 controllerB 推送时键盘无法正确显示。这两种情况之间的区别在于 controllerA 是一个静态 tableview 控制器,而 controllerB 有一个 tableview 控制器,我通过添加自己的搜索控制器使其可搜索。

e.g. controllerA with searchbar segues to controllerB with searchbar

使用多个断点并在不同点检查 searchController 和搜索栏的状态,我能够确定 searchController.canBecomeFirstResponder 正在返回 false。我还发现我需要将 SearchResultsUpdater 设置为 self 以及 searchController 和 searchBar 上的委托。

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

关于动画的一个词 ~ 我发现在将内容分配给 navigationItem 或 navigationBar 时,系统有一些内置的计时和默认动画。我避免在 moveToParent 方法中添加自定义动画、代码或延迟演示,因为在许多情况下会发生意外行为。

为什么这个解决方案有效

definesPresentationContext 上的 Apple documentation 指出了默认行为,并指出了一些情况,在这些情况下,此上下文会调整控制器分配给管理键盘外观的行为。在我的例子中,controllerA 被分配来管理演示而不是 controllerB,所以我只是通过调整这个值来改变这种行为:

When using the currentContext or overCurrentContext style to present a view controller, this property controls which existing view controller in your view controller hierarchy is actually covered by the new content. When a context-based presentation occurs, UIKit starts at the presenting view controller and walks up the view controller hierarchy. If it finds a view controller whose value for this property is true, it asks that view controller to present the new view controller. If no view controller defines the presentation context, UIKit asks the window’s root view controller to handle the presentation. The default value for this property is false. Some system-provided view controllers, such as UINavigationController, change the default value to true.

这就是它在 Swift 3.

中对我有用的地方
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.perform(#selector(self.showKeyboard), with: nil, afterDelay: 0.1)
}

func showKeyboard() {
    self.searchController.searchBar.becomeFirstResponder()
}

Swift4,iOS11

对我有用

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

    resultSearchController.isActive = true
}

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

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

答案是在viewDidAppear中调用becomeFirstResponder。

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    searchBar?.becomeFirstResponder()
}

在Objective C中:

- (void)viewDidLoad {
    [super viewDidLoad];

    // Setup your SearchViewController here...
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    // Will show Cancel button in White colour 
    [[UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UISearchBar class]]] setTintColor:[UIColor whiteColor]];

    // searchController.active = YES;   // This is not necessary

    // set SearchBar first responder inside Main Queue block
    dispatch_async(dispatch_get_main_queue(), ^{
        [self->searchController.searchBar becomeFirstResponder];
    });
}

这就是它在 Swift 4

中对我有用的地方
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    searchController.isActive = true
    DispatchQueue.main.async{
        self.searchController.searchBar.becomeFirstResponder()
    }
}

根据@mislovr 的解决方案,0.1 延迟时间不够长。这是我对该答案的更新代码。

func presentSearchController() {
    searchController.isActive = true

    Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak searchController] timer in
        guard let searchController = searchController else {
            timer.invalidate()
            return
        }

        if searchController.searchBar.canBecomeFirstResponder {
            searchController.searchBar.becomeFirstResponder()
            timer.invalidate()
        }
    }
}

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()
       }
  }
}

Swift 5

class ViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // Only after viewDidAppear searchController can be activated
        searchController?.isActive = true
    }
}

extension ViewController: UISearchControllerDelegate {
    // didPresentSearchController not work for me
    func presentSearchController(_ searchController: UISearchController) {
        searchController.searchBar.becomeFirstResponder()
    }
}

Xcode11.4,Swift5.2

如果您只想显示 SearchBar 但不激活 TextField 和键盘。不需要派发到主线程

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

我按如下方式解决了这个问题:

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

func didPresentSearchController(_ searchController: UISearchController) {

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

视图一出现,您就将 searchControllers 'isActive' 属性 设置为 true。这会触发名为 didPresentSearchController 的委托方法。当它触发时,它意味着 searchcontroller 是可见的,因此可以成为第一响应者。

  1. 定义搜索栏。
  2. SearchBar 文本字段“searchField”选项。
  3. viewDidLoad 或 viewWillAppear 调用代码。

@IBOutlet 弱变量 searchBar: UISearchBar!

让 textFieldUISearchBar = searchBar.value(forKey: "searchField") 作为? UITextField

textFieldUISearchBar?.becomeFirstResponder()