Tengo un UIButton en un UITableView de tableFooterView . La vista de tabla es una subvista de la vista de un UIViewController . El botón en la vista de pie de página funciona bien, excepto que si logra tocarlo demasiado rápido después de arrastrar después de la vista de tabla de modo que necesita recuperarse, el botón registra el toque (el resaltado cambia) pero la acción del botón no se llama.
Esto es una pequeña molestia para los usuarios, pero para mí el principal problema es que mis pruebas de interfaz de usuario tocan el botón demasiado pronto y luego fallan porque el siguiente paso no funciona.
Probé esto en el simulador y en un dispositivo, en mi aplicación y en una aplicación nueva y fresca. Siempre ocurre.
¿Es esto un error en iOS? De cualquier manera, ¿hay alguna solución, al menos por el bien de mis pruebas de interfaz de usuario?
Estoy usando iOS 15.
Aquí hay un código de ejemplo que reproduce el problema:
import UIKit class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { let count = 20 override func viewDidLoad() { super.viewDidLoad() let tableView = UITableView() tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") tableView.delegate = self tableView.dataSource = self let button: UIView = .button(self, action: #selector(onTap)) button.frame = CGRect(origin: .zero, size: .init(width: UIView.noIntrinsicMetric, height: 200)) tableView.tableFooterView = button } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = "Row \(indexPath.row)" return cell } @objc func onTap(_ sender: UIButton) { let alert = UIAlertController(title: "Alert", message: "Message!", preferredStyle: .alert) alert.addAction(.init(title: "OK", style: .default, handler: nil)) present(alert, animated: true, completion: nil) } } extension UIView { static func button(_ target: Any?, action: Selector) -> UIView { let view = UIView() view.isUserInteractionEnabled = true let button = UIButton(type: .system) button.backgroundColor = .green button.layer.cornerRadius = 10 button.setTitle("Press Me", for: .normal) button.translatesAutoresizingMaskIntoConstraints = false view.addSubview(button) button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true button.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true button.widthAnchor.constraint(equalToConstant: 100).isActive = true button.heightAnchor.constraint(equalToConstant: 50).isActive = true button.addTarget(target, action: action, for: .touchUpInside) return view } }Un gif que muestra el problema:
La parte relevante de mi prueba de interfaz de usuario:
extension XCUIElement { func tapFooterButton() { let button = tables.buttons["Press Me"] XCTAssertTrue(button.waitForExistence(timeout: 10)) button.tap() } }La prueba de IU no tiene problemas para encontrar el botón en el pie de página; se desplaza automáticamente hasta el botón, pero a veces intenta tocar el botón demasiado pronto y, por lo tanto, la prueba de la interfaz de usuario se atasca.
¿Quizás el toque del botón se interrumpe por el desplazamiento, como si no se registrara como un retoque en el interior porque la posición del botón se movió?
La solución de prueba de IU que estoy probando actualmente, en caso de que alguien esté interesado, es llamar a tap() en el elemento del botón nuevamente (primero verificando isHittable ) en caso de que no haya funcionado la primera vez. No es ideal pero si funciona, es algo al menos.
Estaba revisando algunas aplicaciones de Apple iOS y parece que este es el comportamiento normal que pretendían desde el punto de vista de UX.
No pude encontrar muchas preguntas similares, sin embargo, encontré esto que parece bastante cercano a lo que desea, sin embargo, lamentablemente el enlace publicado en la respuesta ya no existe.
Un comentario de esa respuesta explica este comportamiento:
UIScrollView retrasa el envío de eventos táctiles hasta que sepa si esos toques no son eventos de desplazamiento. Su problema es que un usuario que toca es un evento de desplazamiento cuando UIScrollView se está moviendo.
Con eso en mente, aquí hay una solución alternativa que podría funcionar, sin embargo, solo debe usarse para probar/depurar, ya que podría evitar que se complete el rebote.
Aquí está mi proceso de pensamiento
UITableView que se ajuste a UIGestureRecognizerDelegategestureRecognizer shouldRecognizeSimultaneouslyWith con para obtener el toque. class CustomTableView: UITableView, UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { // DO THE FOLLOWING ONLY IN DEBUG - IFDEF DEBUG // Get the tap location let tapLocation = gestureRecognizer.location(in: self) // Check if the button was tapped if let button = hitTest(tapLocation, with: nil) as? UIButton { // Programmatically perform the button tap // This might stop the bounce currently on the main thread button.sendActions(for: .touchUpInside) } // ENDIF return true } }Luego se usa normalmente
let tableView = CustomTableView()