UIButton.ConfigurationUpdateHandler slow in UITableView cell [UIButton.Configuration, UIButtonConfiguration]

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?

Answered by Frameworks Engineer in 789868022

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.

Accepted Answer

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.

UIButton.ConfigurationUpdateHandler slow in UITableView cell [UIButton.Configuration, UIButtonConfiguration]
 
 
Q