Cannot set searchBar as firstResponder
Asked Answered
N

22

58

I have a searchBar I'm setting in a tableviewcontroller. i've referenced this similar question UISearchBar cannot become first responder after UITableView did re-appear but am still unable to set it as first responder. In .h file:

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

In view didload:

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;

And in viewDidAppear:

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

When I segue to the view the searchBar animates, but no keyboard appears.

Noachian answered 14/1, 2015 at 20:37 Comment(3)
What if you delay the becomeFirstResponder a bit with dispatch_after()?Ritualist
Is there a reason why you are calling it before super viewDidAppear.Tubercle
I tried putting it before and after with no luck. the search bar is created programmatically, is there some obvious hook up I might have missed?Noachian
D
62

I noticed this issue too. What seems to happen is the call to becomeFirstResponder is done when the searchController is still 'loading'. If you comment out becomeFirstResponder you notice that there is no difference. So we need a way to call becomeFirstResponder after the searchController is 'done' loading.

When I looked at various delegate methods I noticed there is a delegate method:

- (void)didPresentSearchController:(UISearchController *)searchController

This method is called right after the searchController has been presented. Then I make the call to becomeFirstResponder:

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

This fixes the problem. You will notice that when the searchController is loaded, the searchbar now has focus.

Duston answered 15/2, 2015 at 14:31 Comment(11)
This does not work for me in iOS8. Only when I manually touch the SearchBar's text field is didPresentSearchController called.Kat
That's odd, on iOS 8 I have no problems. Something else is wrong in your situation I guessDuston
@Kat I think I know what you are doing wrong; you need to open/activate the searchViewController, e.g. by placing this in your viedDidLoad: [self.searchController setActive:TRUE];Duston
I'm seeing the same problem; iOS 8, searchController.active = true.O
@Kat make sure to also throw the becomeFirstResponder call onto the main thread. For some reason didPresentSearchController is called in some iOS versions in a background thread!Warhol
[self.searchController setActive:TRUE]; must put in viewDidAppear, if put in viewDidLoad, it won't work.Catchall
iOS 9.3 this does not work. mislovr's answer or a dispatch_after is unfortunately the only solution.Graduated
Its working fine for me in 9.3. I put 'searchController.active = true' in viewWillAppear, then the delegate method gets called to make the searcher first responder. I also put a hidden textfield in my view, and made it the first responder, to get the keyboard up quickly.Clance
Didn't work for me on iOS 10. mislovr's solution with delay helped.Yorick
@Yorick this is still working for me on iOS 10, not sure what makes the difference in your situationDuston
Make sure you set searchController.delegate = self. Move searchController.searchBar.becomeFirstResponder() inside DispathQueue.main.async { ... }Lowland
A
48

The solution with - (void)didPresentSearchController:(UISearchController *)searchController did not work, since this delegate method is called only when the user taps on the search bar...

However, this solution did work:

- (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)
}
Argentiferous answered 20/7, 2015 at 8:3 Comment(2)
Or more simply: [self.searchController.searchBar performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0.1];Westerly
This worked for me. Still a valid answer.Delivery
C
19

Well, I found the solution that is actually perfectly working for me.

Don't call [self.searchController setActive:YES]; before calling [self.searchController.searchBar becomeFirstResponder];

What's better, don't call [self.searchController setActive:YES]; at all.

Call only [self.searchController.searchBar becomeFirstResponder]; and the keyboard just pops out as it should, without any delay.

It seems to be somewhat like a bug and a lot of people are confirming it. For example, check here: When assigning focus via becomeFirstResponder to UISearchController's UISearchBar, the keyboard does not appear

Chewy answered 8/6, 2016 at 21:6 Comment(1)
It's not working for me. Where do you call [self.searchController.searchBar becomeFirstResponder]; ?Wormwood
T
16

Swift 4, iOS 11

It works for me

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

    resultSearchController.isActive = true
}

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

    DispatchQueue.main.async {
        searchController.searchBar.becomeFirstResponder()
    }
}
Towland answered 28/2, 2018 at 15:29 Comment(2)
Works for me, note I did need to set self.definesPresentationContext = true in viewWillAppear as well for it to behave correctly.Tom
Not working. Have a look at #31274558Dott
A
15

The function searchController.searchBar.becomeFirstResponder() must be called in the main thread and after searchController.active = true in the viewDidLoad method. Here're the full solution. It works on iOS 9.3

override func viewDidAppear(animated: Bool) {
  super.viewDidAppear(animated)
  searchController.active = true
  Async.main {
    self.searchController.searchBar.becomeFirstResponder()
  }
}
Ashraf answered 16/6, 2016 at 2:10 Comment(3)
Setting searchController.active first worked for me. Why do you need the Async.main call here inside a view lifecycle method, isn't this guaranteed to run on the main thread?Diverticulitis
UISearchController seems to have some unfinished work at this moment (as if they're calling their delegate methods too early), and Async.main is simply scheduling becomeFirstResponder() to happen on a future iteration, bypassing that bug.Butch
You are just getting lucky here that by dispatching it to the main queue it gets executed after the search controller finishes presenting. The correct solution is to call becomeFirstResponder from didPresentSearchController delegate method.Gaw
H
8

Very similar to other answers, but I had to access the main queue in the ViewDidAppear. The SearchBarController can't be acted upon until the View appears and then can only be done so in the main queue for UI:

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

        dispatch_async(dispatch_get_main_queue(), {
            self.searchController.searchBar.becomeFirstResponder()
        });
Houchens answered 7/8, 2016 at 19:57 Comment(2)
NOT setting the active variable made it work for me.Conform
viewDidAppear is always called on the main queue. This code is actually scheduling becomeFirstResponder() to happen on a future iteration of the main queue, allowing UISearchController to finish whatever it hasn't done yet at this moment.Butch
T
7

Easy Swift3 variant:

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

It works ;-)

Tucker answered 6/2, 2017 at 14:11 Comment(2)
Why are you calling DispatchQueue.main.async in viewDidAppear? There is never never any scenario that viewDidAppear will be called on anything other than the main thread so it's not needed at all.Duston
@Duston this can happend and this dispatched solved my issue. Want to give vitya 5 cannon salute.Pede
L
7

Xcode 11.4, Swift 5.2

If you just want the SearchBar to appear but not activate the TextField & keyboard. There is no need to dispatch it to the main thread.

enter image description here

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

If you want the TextField to activate with the keyboard, then you do need to call it on the main thread. There is no need to make the SearchController active, this happens automatically.

enter image description here

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

This may depend on how you configure your SearchController. This is how I configure mine:

// 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
Lodging answered 1/5, 2020 at 9:19 Comment(4)
This actually is the most updated answer. I wanted the second option and it worked in iOS 13.6Incalculable
@Lodging dude.Briquet
This works but it has an issue. I have this search bar in my second view controller after pushing from the first view controller. It works on the first push, but when I tap back and come to this screen again, it shows error Attempt to present UISearchController which is already presenting. To fix it, call this in viewDidLoad: definesPresentationContext = truePham
This is the only solution that works for me on iOS 14. Thanks!Coxcomb
P
6

Swift 5 solution:

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()
       }
  }
}
Phenolphthalein answered 14/6, 2019 at 12:35 Comment(2)
Works great on iOS 11, iOS 12 and iOS 13.Crowther
Yes this works, too bad it takes a moment until the animations finish and the keyboard pops up. Tried viewWillAppear but that did not workSlating
S
5

Alternative Approach

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.

(Source of self.definesPresentationContext solution - See SO answer)

One thing to keep in mind is the context of how the SearchBar is being presented. Embedded in a toolbar, navigation bar, another UIView, as an input or input accessory view. All of which, I've found to have some impact on the timing and internal animation of the search bar as it is being presented or dismissed.

I've attempted all of the solutions presented and none of these worked until I reconsidered how I was trying to use the searchBar. In my case, I was pushing a controller (B) with a search controller from a controller (A) that already had an initial searchcontroller on it. I programmatically embedded each of the search controllers within the titleView of my navigation item when doing a pull refresh.

The answers suggesting adding searchbar.becomeFirstResponder() into the life cycle didn't make sense for my use-case since the view was fully loaded by the time I wanted to insert and display my search bar into the navigationItem. The logic also seemed confusing since the view controller lifecycle methods should already be operating on the main thread. Adding a delay to the presentation also seemed to be an interference with the internal operations of the display and animation used by the system.

I found that calling my function to toggle the insertion worked when I pushed the view controller from controllerA but the keyboard would not display properly when pushed from controllerB. The difference between these two situations was that controllerA was a static tableview controller and controllerB had a tableview controller that I had made searchable by adding its own search controller.

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

Using a number of breakpoints and examining the status of the searchController and the searchbar at different points I was able to determine that the searchController.canBecomeFirstResponder was returning false. I also found that I needed to set the SearchResultsUpdater to self and the delegates on both the searchController and the searchBar.

I finally noted that setting self.definesPresentationContext = true on controllerA was not allowing the keyboard to be displayed when I pushed controllerB onto the navigation stack. My solution was to move the self.definesPresentationContext = true to viewDidAppear on controllerA and in the prepare(for:sender:) method of controllerA I change it to self.definesPresentationContext = false when the destination is controllerB. This resolved the keyboard display issue in my case.

A word on animation ~ I've found that when assigning things to the navigationItem or navigationBar, the system has some built in timing and default animations. I avoid adding custom animation, code in moveToParent methods, or delayed presentations because unexpected behavior occurs in many cases.

Why this solution works

Apple documentation on definesPresentationContext indicates the default behavior and notes some situations where this context adjusts the behavior the controller assigned to manage the keyboard appearance. In my case controllerA was assgined to manage the presentation rather than controllerB, so I just changed that behavior by adjusting this value:

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.

Sunbonnet answered 7/5, 2017 at 18:13 Comment(0)
C
4

This is what it worked for me in Swift 4

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    searchController.isActive = true
    DispatchQueue.main.async{
        self.searchController.searchBar.becomeFirstResponder()
    }
}
Cyclic answered 17/5, 2018 at 22:58 Comment(1)
For iOS15.0, work fine, Thanks.Bilbo
R
4

i fixed this as follows:

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

func didPresentSearchController(_ searchController: UISearchController) {

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

What happens is as soon as the view appears you set your searchControllers 'isActive' property to true. This fires the delegate method called didPresentSearchController. when that fires it means that searchcontroller is visible and thus can become first responder.

Rectitude answered 13/9, 2020 at 4:41 Comment(1)
Code-only answers are discouraged on Stack Overflow because they don't explain how it solves the problem. Please edit your answer to explain what this code does and how it improves on the existing upvoted answers this question already has, so that it is useful to other users with similar issues.Rashid
H
3

The answer is to call becomeFirstResponder in viewDidAppear.

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    searchBar?.becomeFirstResponder()
}
Hypopituitarism answered 26/3, 2018 at 2:18 Comment(0)
L
3

Based on @mislovr's solution, the 0.1 delay was not long enough. Here is my updated code to that answer.

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()
        }
    }
}
Lyckman answered 31/7, 2018 at 12:19 Comment(0)
B
2

I think there might be a cleaner solution. I found that the keyboard was sort of 'blipping' up and then back down when presented, and calling becomeFirstResponder in didPresentSearchController: was working, but the keyboard was coming in late, and the animation was a bit quirky.

Wrapping my reload data method with a check for presentation made everyone happy:

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

I found this by setting a breakpoint in resignFirstResponder. resignFirstResponder was being called from the reloadData, which in turn was called by updateSearchResultsForSearchController:. It turns out that updateSearchResultsForSearchController: is called during the presentation of the search controller. If you muck with the view hierarchy that the UISearchBar is in during this time, the search controller gets borked. My guess is that the reloadData call was causing the UICollectionReusableView header view to come out and go back into the view hierarchy and forcing the UISearchBar subview to resign first responder.

Another symptom I saw was the the search term was not resetting to the middle of the search bar on cancel, which caused it not to present properly on future clicks.

Bandoline answered 22/9, 2015 at 14:43 Comment(1)
So where you are calling becomeFirstResponder now?Schreibman
C
2

The solution of mixing @edwardmp and @mislovr answers kind of worked for me (keyboard pops out with a slight delay):

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

- (void) showKeyboard {
    [self.searchController.searchBar becomeFirstResponder];
}
Chewy answered 24/5, 2016 at 8:27 Comment(0)
C
2

I battled with this for a while but got it working by:

  • Initializing the searchController in viewDidLoad()
  • Setting active = true in viewDidAppear()
    • This triggers didPresentSearchController() in the UISearchControllerDelegate extension.
  • Setting searchBar.becomeFirstResponder() in didPresentSearchController()

Here's the full example, it uses Google Maps Autocomplete.

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()
        }
    }
}
Cuspidation answered 19/8, 2016 at 0:22 Comment(0)
A
2

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()
    }
}
Andris answered 31/1, 2020 at 0:34 Comment(0)
S
1

In 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];
    });
}
Shostakovich answered 2/5, 2018 at 7:42 Comment(0)
T
0

To me, there’s a quite big lag when using viewDidAppear. It can be better to use becomeFirstResponder asynchronously in viewDidLoad (tested with iOS 10, Swift 3):

override func viewDidLoad() {
    super.viewDidLoad()

    DispatchQueue.main.async {
        searchController.searchBar.becomeFirstResponder()
    }
}
Taimi answered 9/2, 2017 at 2:7 Comment(0)
M
0

This is what it worked for me in 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()
}
Momism answered 19/5, 2017 at 22:12 Comment(0)
J
0
  1. Define searchBar.
  2. SearchBar textfield "searchField" choice.
  3. viewDidLoad or viewWillAppear call code.

@IBOutlet weak var searchBar: UISearchBar!

let textFieldUISearchBar = searchBar.value(forKey: "searchField") as? UITextField

textFieldUISearchBar?.becomeFirstResponder()

Jodi answered 21/5, 2021 at 8:39 Comment(2)
Code-only answers are not particularly helpful. Please add some descriptions of how this code solves the problem.Carpetbag
While this code may answer the question, providing additional context regarding how and/or why it solves the problem would improve the answer's long-term value.Grishilde

© 2022 - 2024 — McMap. All rights reserved.