I have a UIButton
in a UITableView
‘s tableFooterView
. The table view is a subview of the view of a UIViewController
. The button in the footer view works fine except that if you manage to tap it too quickly after dragging after the table view such that it needs to bounce back, the button registers the touch (the highlighting changes) but the button’s action is not called.
This is a small annoyance for users but for me the primary problem is that my UI tests are tapping the button too early and then failing because the next step doesn’t work.
I’ve tried this out in the simulator and on a device, in my app and in a fresh new app. It always occurs.
Is this a bug in iOS? Either way, is there a workaround, at least for the sake of my UI tests?
I am using iOS 15.
Here is some example code which reproduces the issue:
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
}
}
A gif showing the issue:
The relevant part of my UI test:
extension XCUIElement {
func tapFooterButton() {
let button = tables.buttons["Press Me"]
XCTAssertTrue(button.waitForExistence(timeout: 10))
button.tap()
}
}
The UI test has no trouble finding the button in the footer; it scrolls automatically to the button, but sometimes it tries tapping the button too early and so the UI test gets stuck.
Maybe the button tap is being interrupted by the scrolling, like it’s not registering as touch up inside because the position of the button moved?
2
Answers
UI test workaround that I'm currently trying, in case anyone is interested, is to call
tap()
on the button element again (first checking forisHittable
) in case it didn't work the first time. It's not ideal but if it works, it's something at least.I was just checking some Apple iOS apps and it seems this is the normal behavior they intended from a UX point of view.
I could not find many similar questions, however I came across this which seems quite close to what you want, however, the link posted in the answer no longer exists sadly.
One comment from that answer explains this behavior:
With that in mind, here is one workaround that might work, however, it should only be used for testing / debug as it might preventing the bounce from completing.
Here is my thought process
UITableView
subclass that conforms toUIGestureRecognizerDelegate
gestureRecognizer shouldRecognizeSimultaneouslyWith
in order to get the touchThen used normally