If you run the following UIKit app and tap on the button, you can see that it only updates its color if you hold on it for a bit, instead of immediately (as happens in the second app) (iOS 17.5, iPhone 15 Pro simulator, Xcode 15.4).
This app consists of a view controller with a table view with one cell, which has a CheckoutButton
instance constrained to its contentView
top, bottom, leading and trailing anchors.
The checkout button uses UIButton.Configuration to set its appearance, and update it based on its state.
import UIKit
class ViewController: UIViewController {
let tableView = UITableView()
let checkoutButton = CheckoutButton()
override func viewDidLoad() {
super.viewDidLoad()
// table view setup
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.contentView.addSubview(checkoutButton)
checkoutButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
checkoutButton.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
checkoutButton.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor),
checkoutButton.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor),
checkoutButton.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor)
])
return cell
}
}
class CheckoutButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
var configuration = UIButton.Configuration.plain()
var attributeContainer = AttributeContainer()
attributeContainer.font = .preferredFont(forTextStyle: .headline)
attributeContainer.foregroundColor = .label
configuration.attributedTitle = .init("Checkout", attributes: attributeContainer)
self.configuration = configuration
let configHandler: UIButton.ConfigurationUpdateHandler = { button in
switch button.state {
case .selected, .highlighted:
button.configuration?.background.backgroundColor = .systemCyan
case .disabled:
button.configuration?.background.backgroundColor = .systemGray4
default:
button.configuration?.background.backgroundColor = .systemBlue
}
}
self.configurationUpdateHandler = configHandler
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In this second app, instead, the selection of the button is immediately reflected in its appearance:
import UIKit
class ViewController: UIViewController {
let button = CheckoutButton()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
button.widthAnchor.constraint(equalToConstant: 300),
button.heightAnchor.constraint(equalToConstant: 44)
])
}
}
class CheckoutButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
var configuration = UIButton.Configuration.plain()
var attributeContainer = AttributeContainer()
attributeContainer.font = .preferredFont(forTextStyle: .headline)
attributeContainer.foregroundColor = .label
configuration.attributedTitle = .init("Checkout", attributes: attributeContainer)
self.configuration = configuration
let configHandler: UIButton.ConfigurationUpdateHandler = { button in
switch button.state {
case .selected, .highlighted:
button.configuration?.background.backgroundColor = .systemCyan
case .disabled:
button.configuration?.background.backgroundColor = .systemGray4
default:
button.configuration?.background.backgroundColor = .systemBlue
}
}
self.configurationUpdateHandler = configHandler
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
This app consists of a view controller with just a button: no table view unlike in the first app.
How do I make the button show its selection as soon as it's tapped, no matter if it's in a table view cell or on its own?
UIScrollView delays touch-down event delivery when its delaysContentTouches
property is set to true
, which is the default value. See https://developer.apple.com/documentation/uikit/uiscrollview/1619398-delayscontenttouches
If you set that to false
on the UITableView, you should see buttons inside of it react immediately and highlight on touch-down. Keep in mind that this may not be desirable when the user intends to scroll, which is why this short delay is in place by default: it helps avoid undesired flashing due to content reacting to the touch-down event when it is quickly cancelled as the user begins a pan gesture to scroll.