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?
