Search code examples
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)))
    }