iosswiftuikituibuttonuistackview

UIStackView wrong distribution if subviews are UIButton created with UIButton.Configuration


The UIStackView doesn't have the correct .fill distribution if subviews are UIButtons created with UIButton.Configuration.

In the example below I created two UIStackView with subviews of UIButton type. The two stackviews are the same, but subview UIButtons are created in different way

let stackView1 = UIStackView()
stackView1.translatesAutoresizingMaskIntoConstraints = false
stackView1.distribution = .fill

If UIButtons are created with regular constructors,

let button = UIButton()

the .fill distribution works as expected (look at the top stack view in the attached image)

If UIButtons are created with Configuration passed to the constructor

var configuration = UIButton.Configuration.plain()
let button = UIButton(configuration: configuration)

the parent UIStackView doesn't provide correct fill distribution (bottom stack view in the attached image).

How to make UIStackView provide the correct distribution for UIButtons created with Configuration?

env: XCode 15.4, Simulator iOS 17.5

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
     
    let stackView1 = UIStackView()
    stackView1.translatesAutoresizingMaskIntoConstraints = false
    stackView1.distribution = .fill
    stackView1.alignment = .fill
     
    let stackView2 = UIStackView()
    stackView2.translatesAutoresizingMaskIntoConstraints = false
    stackView2.distribution = .fill
    stackView2.alignment = .fill
     
    view.addSubview(stackView1)
    view.centerXAnchor.constraint(equalTo: stackView1.centerXAnchor).isActive = true
    view.centerYAnchor.constraint(equalTo: stackView1.centerYAnchor).isActive = true
     
    view.addSubview(stackView2)
    view.centerXAnchor.constraint(equalTo: stackView2.centerXAnchor).isActive = true
    stackView2.topAnchor.constraint(equalTo: stackView1.bottomAnchor, constant: 40).isActive = true
     
    stackView1.addArrangedSubview(makeButton("Button", backgroundColor: .red))
    stackView1.addArrangedSubview(makeButton("Button Very Wild!!", backgroundColor: .blue))
    stackView1.addArrangedSubview(makeButton("Button mid size", backgroundColor: .yellow))
    stackView1.addArrangedSubview(makeButton("But", backgroundColor: .cyan))
     
    stackView2.addArrangedSubview(makeButtonWithConfiguration("Button", backgroundColor: .red))
    stackView2.addArrangedSubview(makeButtonWithConfiguration("Button Very Wild!!", backgroundColor: .blue))
    stackView2.addArrangedSubview(makeButtonWithConfiguration("Button mid size", backgroundColor: .yellow))
    stackView2.addArrangedSubview(makeButtonWithConfiguration("But", backgroundColor: .cyan))
  }

  func makeButton(_ title: String, backgroundColor: UIColor) -> UIButton {
    let button = UIButton()
    button.setTitle(title, for: .normal)
    button.backgroundColor = backgroundColor
    return button
  }
   
  func makeButtonWithConfiguration(_ title: String, backgroundColor: UIColor) -> UIButton {
    var configuration = UIButton.Configuration.filled()
    configuration.title = title
    configuration.baseBackgroundColor = backgroundColor
    let button = UIButton(configuration: configuration)
    return button
  }
}

enter image description here


Solution

  • The default UIButton.Configuration.titleLineBreakMode is .byWordWrapping, which means the title can be multi-line. Compare this to the default lineBreakMode of a UIButton without Configuration - .byTruncatingTail, which means the title can only be one line.

    You can set it to .byClipping, so that the title is only one line. As a result, there is a "natural size" for the button.

    var configuration = UIButton.Configuration.plain()
    configuration.titleLineBreakMode = .byTruncatingTail
    
    // you can also remove these insets and set the font size
    // if you want it to look exactly like the buttons without a configuration
    configuration.contentInsets.leading = 0
    configuration.contentInsets.trailing = 0
    configuration.titleTextAttributesTransformer = .init {
        $0.merging(.init().font(UIFont.systemFont(ofSize: 18)))
    }