user9150170
user9150170

Reputation:

Create UIButtons with dynamic font size but all share same font size in UIStackView

I am using UIStackView and adding three buttons to it. I want it so that the button with the most text (B1) will be auto resized to fit the width and the other buttons will share the same font size as B1.

@IBOutlet weak var stackView: UIStackView!

var btnTitles = [String]()
btnTitles.append("Practice Exams")
btnTitles.append("Test Taking Tips")
btnTitles.append("About")
createButtons(buttonTitles: btnTitles)

var min = CGFloat(Int.max) // keep track of min font

func createButtons(buttonTitles: [String]) {

    var Buttons = [UIButton]()

    for title in buttonTitles {
        let button = makeButtonWithText(text: title)
        // set the font to dynamically size
        button.titleLabel!.numberOfLines = 1
        button.titleLabel!.adjustsFontSizeToFitWidth = true
        button.titleLabel!.baselineAdjustment = .alignCenters // I think it keeps it centered vertically
        button.contentEdgeInsets = UIEdgeInsetsMake(5, 10, 5, 10); // set margins
        if (button.titleLabel?.font.pointSize)! < min {
            min = (button.titleLabel?.font.pointSize)! // to get the minimum font size of any of the buttons
        }

        stackView.addArrangedSubview(button)
        Buttons.append(button)
    }
}

func makeButtonWithText(text:String) -> UIButton {
    var myButton = UIButton(type: UIButtonType.system)
    //Set a frame for the button. Ignored in AutoLayout/ Stack Views
    myButton.frame = CGRect(x: 30, y: 30, width: 150, height: 100)
    // background color - light blue
    myButton.backgroundColor = UIColor(red: 0.255, green: 0.561, blue: 0.847, alpha: 1)

    //State dependent properties title and title color
    myButton.setTitle(text, for: UIControlState.normal)
    myButton.setTitleColor(UIColor.white, for: UIControlState.normal)

    // set the font to dynamically size
    myButton.titleLabel!.font = myButton.titleLabel!.font.withSize(70)
    myButton.contentHorizontalAlignment = .center // align center

    return myButton
}

I wanted to find the minimum font size and then set all the buttons to the minimum in viewDidAppear button the font prints as 70 for all of them even though they clearly appear different sizes (see image)

override func viewDidAppear(_ animated: Bool) {
    print("viewDidAppear")

    let button = stackView.arrangedSubviews[0] as! UIButton
    print(button.titleLabel?.font.pointSize)
    let button1 = stackView.arrangedSubviews[1] as! UIButton
    print(button1.titleLabel?.font.pointSize)
    let button2 = stackView.arrangedSubviews[2] as! UIButton
    print(button2.titleLabel?.font.pointSize)
}

image

Upvotes: 0

Views: 401

Answers (1)

DonMag
DonMag

Reputation: 77511

You can try playing around with this func to return the scaled-font-size of a label:

    func actualFontSize(for aLabel: UILabel) -> CGFloat {

        // label must have text, must have .minimumScaleFactor and must have .adjustsFontSizeToFitWidth == true
        guard let str = aLabel.text,
            aLabel.minimumScaleFactor > 0.0,
            aLabel.adjustsFontSizeToFitWidth
            else { return aLabel.font.pointSize }

        let attributes = [NSAttributedString.Key.font : aLabel.font]
        let attStr = NSMutableAttributedString(string:str, attributes:attributes as [NSAttributedString.Key : Any])

        let context = NSStringDrawingContext()
        context.minimumScaleFactor = aLabel.minimumScaleFactor

        _ = attStr.boundingRect(with: aLabel.bounds.size, options: .usesLineFragmentOrigin, context: context)

        return aLabel.font.pointSize * context.actualScaleFactor

    }

On viewDidAppear() you would loop through the buttons, getting the smallest actual font size, then set the font size for each button to that value.

It will take some experimentation... For one thing, I've noticed in the past that font-sizes can get rounded - so setting a label's font point size to 20.123456789 won't necessarily give you that exact point size. Also, since this changes the actual font size assigned to the labels, you'll need to do some resetting if you change the button title dynamically. Probably also need to account for button frame changes (such as with device rotation, etc).

But... here is a quick test that you can run to see the approach:

class TestViewController: UIViewController {

    let stackView: UIStackView = {
        let v = UIStackView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.axis = .vertical
        v.alignment = .center
        v.distribution = .fillEqually
        v.spacing = 8
        return v
    }()

    var btnTitles = [String]()
    var theButtons = [UIButton]()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        fixButtonFonts()
    }

    func setupUI() -> Void {

        view.addSubview(stackView)

        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
            stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40),
            stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40),
            ])

        btnTitles.append("Practice Exams")
        btnTitles.append("Test Taking Tips")
        btnTitles.append("About")
        createButtons(buttonTitles: btnTitles)

    }

    func fixButtonFonts() -> Void {

        var minActual = CGFloat(70)

        // get the smallest actual font size
        theButtons.forEach { btn in
            if let lbl = btn.titleLabel {
                let act = actualFontSize(for: lbl)
                // for debugging
                //print("actual font size: \(act)")
                minActual = Swift.min(minActual, act)
            }
        }

        // set font size for each button
        theButtons.forEach { btn in
            if let lbl = btn.titleLabel {
                lbl.font = lbl.font.withSize(minActual)
            }
        }

    }

    func createButtons(buttonTitles: [String]) {

        for title in buttonTitles {
            let button = makeButtonWithText(text: title)
            // set the font to dynamically size
            button.titleLabel!.numberOfLines = 1
            button.titleLabel!.adjustsFontSizeToFitWidth = true
            // .minimumScaleFactor is required
            button.titleLabel!.minimumScaleFactor = 0.05
            button.titleLabel!.baselineAdjustment = .alignCenters // I think it keeps it centered vertically
            button.contentEdgeInsets = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10); // set margins

            stackView.addArrangedSubview(button)
            theButtons.append(button)
        }

    }

    func makeButtonWithText(text:String) -> UIButton {
        let myButton = UIButton(type: UIButton.ButtonType.system)
        //Set a frame for the button. Ignored in AutoLayout/ Stack Views
        myButton.frame = CGRect(x: 30, y: 30, width: 150, height: 100)
        // background color - light blue
        myButton.backgroundColor = UIColor(red: 0.255, green: 0.561, blue: 0.847, alpha: 1)

        //State dependent properties title and title color
        myButton.setTitle(text, for: UIControl.State.normal)
        myButton.setTitleColor(UIColor.white, for: UIControl.State.normal)

        // set the font to dynamically size
        myButton.titleLabel!.font = myButton.titleLabel!.font.withSize(70)
        myButton.contentHorizontalAlignment = .center // align center

        return myButton
    }

    func actualFontSize(for aLabel: UILabel) -> CGFloat {

        // label must have text, must have .minimumScaleFactor and must have .adjustsFontSizeToFitWidth == true
        guard let str = aLabel.text,
            aLabel.minimumScaleFactor > 0.0,
            aLabel.adjustsFontSizeToFitWidth
            else { return aLabel.font.pointSize }

        let attributes = [NSAttributedString.Key.font : aLabel.font]
        let attStr = NSMutableAttributedString(string:str, attributes:attributes as [NSAttributedString.Key : Any])

        let context = NSStringDrawingContext()
        context.minimumScaleFactor = aLabel.minimumScaleFactor

        _ = attStr.boundingRect(with: aLabel.bounds.size, options: .usesLineFragmentOrigin, context: context)

        return aLabel.font.pointSize * context.actualScaleFactor

    }

}

Result:

enter image description here

Upvotes: 0

Related Questions