uikituibutton

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?


Solution

  • Set the delaysContentTouches property of the table view to false.

    Credit: https://forums.developer.apple.com/forums/thread/756542.