ios - No se puede establecer searchBar como firstResponder
objective-c uisearchbar (17)
Enfoque alternativo
Esta respuesta analiza una serie de cuestiones relacionadas con esta ocurrencia y explica la lógica de depuración utilizada para aislar y determinar la causa específica del problema en mi caso. En el camino, comparto otras posibilidades que podrían haber funcionado en otras situaciones y explico por qué esto funciona en mi situación.
tldr ; Mire self.definesPresentationContext = true, isbeingDismissed, isBeingPresented, canBecomeFirstResponder y delegue la asignación de SearchController, Searchbar, SearchResultsUpdater y sus métodos delegados.
( Fuente de la solución self.definesPresentationContext
- Vea la respuesta SO )
Una cosa a tener en cuenta es el contexto de cómo se está presentando SearchBar. Incrustado en una barra de herramientas, barra de navegación, otra UIView, como una vista de entrada de entrada o entrada. Todo lo cual, he encontrado que tiene algún impacto en el tiempo y la animación interna de la barra de búsqueda a medida que se presenta o descarta.
Intenté todas las soluciones presentadas y ninguna de ellas funcionó hasta que reconsideré cómo estaba tratando de usar la barra de búsqueda. En mi caso, estaba presionando un controlador (B) con un controlador de búsqueda de un controlador (A) que ya tenía un controlador de búsqueda inicial. Programé incrustado cada uno de los controladores de búsqueda dentro de titleView de mi elemento de navegación al hacer una actualización de extracción.
Las respuestas que sugerían agregar searchbar.becomeFirstResponder()
en el ciclo de vida no tenían sentido para mi caso de uso ya que la vista estaba completamente cargada cuando quería insertar y mostrar mi barra de búsqueda en el elemento de navegación. La lógica también parecía confusa ya que los métodos del ciclo de vida del controlador de visualización ya deberían estar operando en el hilo principal. Agregar un retraso a la presentación también parecía ser una interferencia con las operaciones internas de la pantalla y la animación utilizada por el sistema.
Descubrí que llamar a mi función para alternar la inserción funcionaba cuando empujaba el controlador de vista desde la controladora A pero el teclado no se visualizaba correctamente cuando se lo empujaba desde el controladorB. La diferencia entre estas dos situaciones era que el controllerA era un controlador de tabla de visión estático y el controladorB tenía un controlador de tabla de vista que yo había hecho que se pudiera buscar agregando su propio controlador de búsqueda.
eg controllerA con la barra de búsqueda segues al controllerB con la barra de búsqueda
Utilizando varios puntos de interrupción y examinando el estado de searchController y la barra de búsqueda en diferentes puntos, pude determinar que searchController.canBecomeFirstResponder
devolvía false
. También encontré que necesitaba establecer SearchResultsUpdater
en self
y los delegados en searchController y searchBar.
Finalmente noté que establecer self.definesPresentationContext = true
en el controlador A no permitía que se mostrara el teclado cuando self.definesPresentationContext = true
el controlador self.definesPresentationContext = true
en la pila de navegación. Mi solución fue mover self.definesPresentationContext = true
para viewDidAppear
en controllerA y en el método prepare(for:sender:)
del controllerA lo cambio a self.definesPresentationContext = false
cuando el destino es controllerB. Esto resolvió el problema de visualización del teclado en mi caso.
Una palabra sobre animación ~ He encontrado que al asignar cosas al elemento de navegación o barra de navegación, el sistema tiene algunas temporizaciones y animaciones predeterminadas incorporadas. Evito agregar animación personalizada, código en métodos moveToParent o presentaciones retrasadas porque en muchos casos ocurre un comportamiento inesperado.
Por qué esta solución funciona
La documentation Apple sobre definesPresentationContext
indica el comportamiento predeterminado y toma nota de algunas situaciones en las que este contexto ajusta el comportamiento que el controlador asignó para administrar la apariencia del teclado. En mi caso, el controlador A se asignó para gestionar la presentación en lugar del controlador B, así que simplemente cambié ese comportamiento ajustando este valor:
Cuando se usa el estilo currentContext o overCurrentContext para presentar un controlador de vista, esta propiedad controla qué controlador de vista existente en su jerarquía de controlador de vista está realmente cubierto por el nuevo contenido. Cuando se produce una presentación basada en el contexto, UIKit comienza en el controlador de vista de presentación y sube la jerarquía del controlador de vista. Si encuentra un controlador de vista cuyo valor para esta propiedad es verdadero, solicita que el controlador de vista presente el nuevo controlador de vista. Si ningún controlador de vista define el contexto de la presentación, UIKit le pide al controlador de vista raíz de la ventana que maneje la presentación. El valor predeterminado para esta propiedad es falso. Algunos controladores de vista proporcionados por el sistema, como UINavigationController, cambian el valor predeterminado a verdadero.
Tengo una barra de búsqueda que estoy configurando en un controlador de tabla. Hice referencia a esta pregunta similar. UISearchBar no puede convertirse en el primero en responder después de que UITableView volvió a aparecer, pero todavía no puedo configurarlo como primera respuesta. En archivo .h:
@property (strong, nonatomic) UISearchController *searchController;
@property (strong, nonatomic) IBOutlet UISearchBar *searchBar;
En vista 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;
Y en viewDidAppear:
-(void)viewDidAppear:(BOOL)animated {
[self.searchController setActive:YES];
[self.searchController.searchBar becomeFirstResponder];
[super viewDidAppear:animated];
}
Cuando paso a la vista, la barra de búsqueda se anima, pero no aparece el teclado.
Basado en la solución de @ mislovr, la demora 0.1
no fue lo suficientemente larga. Aquí está mi código actualizado para esa respuesta.
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()
}
}
}
Bueno, encontré la solución que en realidad funciona perfectamente para mí.
No llame [self.searchController setActive:YES];
antes de llamar a [self.searchController.searchBar becomeFirstResponder];
Lo que es mejor, no llame [self.searchController setActive:YES];
en absoluto.
Llamar solo [self.searchController.searchBar becomeFirstResponder];
y el teclado simplemente aparece como debería, sin ningún retraso.
Parece ser algo así como un error y mucha gente lo confirma. Por ejemplo, compruebe aquí: al asignar foco a través de becomeFirstResponder a UISearchBar de UISearchController, el teclado no aparece
Creo que podría haber una solución más limpia. Descubrí que el teclado estaba como ''parpadeando'' y luego volviendo a bajar cuando se presentaba, y llamando a becomeFirstResponder
en didPresentSearchController:
funcionaba, pero el teclado llegaba tarde y la animación era un poco peculiar.
Envolver mi método de recarga de datos con un cheque de presentación hizo felices a todos:
- (void)didPresentSearchController:(UISearchController *)searchController {
[self performSelector:@selector(showKeyboard) withObject:nil afterDelay:0.001];
}
- (void) showKeyboard {
[self.searchController.searchBar becomeFirstResponder];
}
Encontré esto estableciendo un punto de interrupción en resignFirstResponder
. se llamaba a reloadData
desde reloadData
, que a su vez fue llamado por updateSearchResultsForSearchController:
Resulta que se llama a updateSearchResultsForSearchController:
durante la presentación del controlador de búsqueda. Si se ensucia con la jerarquía de vista en la que se encuentra UISearchBar durante este tiempo, el controlador de búsqueda se vuelve loco. Supongo que la llamada a reloadData estaba causando que la vista del encabezado UICollectionReusableView
salga y regrese a la jerarquía de vistas y obligando a la subvista UISearchBar a renunciar al primer respondedor.
Otro síntoma que vi fue que el término de búsqueda no se restablecía en el medio de la barra de búsqueda al cancelar, lo que hizo que no se presentara correctamente en futuros clics.
En el Objetivo 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];
});
}
Esto es lo que funcionó para mí en 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()
}
Esto es lo que funcionó para mí en Swift 4
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchController.isActive = true
DispatchQueue.main.async{
self.searchController.searchBar.becomeFirstResponder()
}
}
La función searchController.searchBar.becomeFirstResponder()
debe invocarse en el hilo principal y después de searchController.active = true
en el método viewDidLoad. Aquí está la solución completa. Funciona en iOS 9.3
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
searchController.active = true
Async.main {
self.searchController.searchBar.becomeFirstResponder()
}
}
La respuesta es llamar a becomeFirstResponder en viewDidAppear.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchBar?.becomeFirstResponder()
}
La solución con - (void)didPresentSearchController:(UISearchController *)searchController
no funcionó, ya que este método de delegado se (void)didPresentSearchController:(UISearchController *)searchController
solo cuando el usuario toca la barra de búsqueda ...
Sin embargo, esta solución funcionó:
- (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)
}
La solución de mezclar @edwardmp y @mislovr responde algo así como funcionó (el teclado se abre con un ligero retraso):
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()
}
}
}
Luché con esto por un tiempo pero lo hice funcionar por:
- Inicializando
viewDidLoad()
enviewDidLoad()
- Configuración
active = true
enviewDidAppear()
- Esto desencadena
didPresentSearchController()
en la extensiónUISearchControllerDelegate
.
- Esto desencadena
- Configuración de
searchBar.becomeFirstResponder()
endidPresentSearchController()
Aquí está el ejemplo completo, usa Autocompletar de Google Maps.
// 1.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
resultSearchController.isActive = true
}
// 2. ->> UISearchControllerDelegate
func didPresentSearchController(_ searchController: UISearchController) {
DispatchQueue.main.async {
searchController.searchBar.becomeFirstResponder()
}
}
Me di cuenta de este problema también. Lo que parece suceder es que la llamada a becomeFirstResponder
se realiza cuando searchController todavía está ''cargando''. Si comenta en becomeFirstResponder
, observa que no hay diferencia. Entonces necesitamos una forma de llamar a becomeFirstResponder después de que SearchController esté ''listo'' para cargar.
Cuando miré varios métodos de delegado noté que hay un método delegado:
- (void)didPresentSearchController:(UISearchController *)searchController
Este método se llama justo después de que se haya presentado searchController. Luego realizo la llamada para becomeFirstResponder
en becomeFirstResponder
:
- (void)didPresentSearchController:(UISearchController *)searchController
{
[searchController.searchBar becomeFirstResponder];
}
Esto soluciona el problema. Notará que cuando se carga searchController, la barra de búsqueda ahora tiene foco.
Muy similar a otras respuestas, pero tuve que acceder a la cola principal en ViewDidAppear
. No se puede actuar sobre SearchBarController
hasta que aparezca la View
y luego solo se puede hacer en la cola principal para la UI
:
searchController.active = true // doubtful whether this is needed
dispatch_async(dispatch_get_main_queue(), {
self.searchController.searchBar.becomeFirstResponder()
});
Para mí, hay un retraso bastante grande al usar viewDidAppear
. Puede ser mejor utilizar becomeFirstResponder
asincrónicamente en viewDidLoad
(probado con iOS 10, Swift 3):
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.async {
searchController.searchBar.becomeFirstResponder()
}
}
Swift 4, iOS 11
Esto funciona para mi
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
if (!searchController.isBeingPresented && !searchController.isBeingDismissed) {
[self.collectionView reloadData];
}
}
Variante Easy 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()
}
Funciona ;-)