0

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.

enter image description here

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:

  1. Dynamically increase height of UIButton to accommodate the title fully.
  2. Ideally add a little padding around the title so the text is not right against the edge.

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 textpost scrolling

spilling text

Nicholas Farmer
  • 773
  • 7
  • 32
  • Try changing `cell.buttonStack.distribution = .fillProportionally` to `cell.buttonStack.distribution = .equalSpacing`. That will allow each button to keeps its own height. – HangarRash Apr 04 '23 at 17:44
  • Unrelated, but most of the code that you have in `cellForRowAt` should be in your custom cell class. `cellForRowAt` should only pass data to the custom cell class. Let the cell class setup its own views. – HangarRash Apr 04 '23 at 17:45
  • "Old style" buttons do not support multi-line titles (you have to subclass them). If you are targeting iOS 15+ you can use the new `UIButton.Configuration` format for your buttons... automatically supports multi-line titles. – DonMag Apr 04 '23 at 18:14
  • https://stackoverflow.com/users/6257435/donmag I've seen this answer: https://stackoverflow.com/questions/67130849/how-do-you-add-multi-line-text-to-a-uibutton-and-have-its-height-adjust-accordi - will this work with a programmatically created button only? Seemed to suggest part storyboard but can't seem to get it working – Nicholas Farmer Apr 04 '23 at 20:24
  • @HangarRash first comment - changing UIStackView doesn't seem to have an effect. Some button will be bigger than others (depending on text content) – Nicholas Farmer Apr 04 '23 at 20:26
  • @HangarRash second comment, yes, I'm in the setup stage so I start coding in cellForRowAt and then move across to custom cell class afterwards. Not saying it's good practice, it's just how I progress. You're right and will move it! – Nicholas Farmer Apr 04 '23 at 20:27
  • @NicholasFarmer - yes, any subclass can be used programmatically. See my answer for a quick example of using a `MultilineTitleButton` class. – DonMag Apr 04 '23 at 21:10

1 Answers1

1

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:

enter image description here


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):

enter image description here

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Great, makes sense. Appreciated – Nicholas Farmer Apr 04 '23 at 21:17
  • BTW, you state 'If you don't want to use the newer style buttons' - I did see the multiline button mentioned for iOS15. Any pointers (tutorial or general advice on how to programmatically add the newer type Unbuttons to a stack view? – Nicholas Farmer Apr 21 '23 at 21:24