I have UITableview with custom cells. In each cell I have on one side a vertical UIStack view of UIButtons. The number of the buttons per stack varies per cell.
As part of the code for the cell I create an array of UIButtons, each with a different title, then add these to a the UIStack via addArrangedSubview(). The UIButtons are created solely in code, while the UIStack is created on storyboard with some overrides in code.
All works well.
However, if the title is larger than 1 line on the button the UIButton doesn't resize to accommodate. On the image, note the overlapping text on the 2nd button next to Consciousness.
Code (all within the UITableview cellatrow function:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: FormVerticalCell.reuseIdentifier, for: indexPath) as? FormVerticalCell else {
fatalError("Unexpected Index Path")
}
for arrangedSubview in cell.buttonStack.arrangedSubviews {
cell.buttonStack.removeArrangedSubview(arrangedSubview)
}
let tempOptions = formArray[indexPath.row][1]
let optionsArray = tempOptions.components(separatedBy: ",")
var btnArray = [UIButton]()
for i in 0..<optionsArray.count {
let button = UIButton()
button.changesSelectionAsPrimaryAction = true
button.setTitle(optionsArray[i], for: .normal)
button.titleLabel?.numberOfLines = 0
button.titleLabel?.lineBreakMode = .byWordWrapping
button.titleLabel?.textAlignment = .left
button.setTitleColor(.black, for: .normal)
button.setTitleColor(.white, for: .selected)
button.setTitleColor(.white, for: .highlighted)
button.tag = i
button.setBackgroundColor(UIColor(red:0.9, green:0.9, blue:0.9, alpha:1.0), forState: .normal)
button.setBackgroundColor(UIColor(red:0.8, green:0.6, blue:0.0, alpha:1.0), forState: .highlighted)
button.setBackgroundColor(UIColor(red:0.8, green:0.6, blue:0.0, alpha:1.0), forState: .selected)
btnArray.append(button)
let tempOptionsScores = formArray[indexPath.row][2]
button.addAction { [self] in
let optionsScoresArray = tempOptionsScores.components(separatedBy: ",")
let tempScore = optionsScoresArray[button.tag]
scoreArray[indexPath.row] = Int(tempScore)!
updateScore()
for i in 0..<btnArray.count
{
if (i != button.tag) {
btnArray[i].isSelected = false
}
}
}
}
for i in 0..<btnArray.count {
cell.buttonStack.addArrangedSubview(btnArray[i])
}
cell.buttonStack.axis = .vertical
cell.buttonStack.alignment = .fill
cell.buttonStack.distribution = .fillProportionally
cell.buttonStack.spacing = 2.0
cell.buttonStack.translatesAutoresizingMaskIntoConstraints = false
cell.label.text = formArray[indexPath.row][0]
// WHY NOT BLACK!!!
cell.label.textColor = .black
return cell
}
Already looked at a few questions - no current up to date answers I could find. Any advice on steps I need to take to:
UPDATE
Seemed to be working well. But randomly if a button has more than 2 lines of text, then the text spills out over the edges. However if the tableview is long enough to scroll - scrolling up and down somehow fixes this (see 2 images below). I presume some issue with the table cell height creation (automatic) - but unsure what, and why only occurs if > 2 lines of text
If you don't want to use the newer style buttons that automatically handle multi-line titles, you can use a subclassed button like this:
class MultilineTitleButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() -> Void {
self.titleLabel?.numberOfLines = 0
self.titleLabel?.textAlignment = .center
}
override var intrinsicContentSize: CGSize {
let size = self.titleLabel!.intrinsicContentSize
return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
}
}
A sample view controller that adds 4 buttons in a vertical stack view, with 200-points width:
class MultilineButtonVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 8
// do NOT use .fillEqually
stackView.distribution = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stackView.widthAnchor.constraint(equalToConstant: 200.0),
])
let titles: [String] = [
"Short.",
"A little longer.",
"This title is long enough to wrap onto multiple lines.",
"A fourth button in the stackView.",
]
titles.forEach { str in
let b = MultilineTitleButton()
b.setTitle(str, for: [])
b.setTitleColor(.black, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.backgroundColor = .yellow
b.contentEdgeInsets = .init(top: 6.0, left: 8.0, bottom: 6.0, right: 8.0)
stackView.addArrangedSubview(b)
}
}
}
Looks like this:
Edit - in response to comments...
Same thing as above, but using the iOS 15+ UIButton.Configuration
style buttons - so, no need for the custom MultilineTitleButton
class:
class MultilineConfigButtonVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 8
// do NOT use .fillEqually
stackView.distribution = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stackView.widthAnchor.constraint(equalToConstant: 200.0),
])
let titles: [String] = [
"Short.",
"A little longer.",
"This title is long enough to wrap onto multiple lines.",
"A fourth button in the stackView.",
]
// let's make the button titles
// 18-point system font
// centered horizontally
let centerStyle = NSMutableParagraphStyle()
centerStyle.alignment = NSTextAlignment.center
var ac = AttributeContainer()
ac.font = .systemFont(ofSize: 18.0)
ac.paragraphStyle = centerStyle
titles.forEach { str in
var cfg = UIButton.Configuration.filled()
cfg.attributedTitle = AttributedString(str, attributes: ac)
cfg.baseBackgroundColor = UIColor(white: 0.9, alpha: 1.0)
cfg.baseForegroundColor = .black
let b = UIButton(configuration: cfg)
stackView.addArrangedSubview(b)
}
}
}
Looks about the same... (using a very light gray instead of yellow for the background):