iosuibuttonuikitaccessibilitydynamic-type-feature

Dynamic type for UIButton's attributeTitle


Anyone ever got dynamic type working for attributedTitle on UIButton? Consider the super simple code below:

let font = UIFont(name: "Helvetica", size: 14)!
let scaledFont = UIFontMetrics.default.scaledFont(for: font)

let button = UIButton(type: .custom)
button.titleLabel?.font = scaledFont
button.titleLabel?.adjustsFontForContentSizeCategory = true

let attributes: [NSAttributedString.Key: Any] = [ .font: scaledFont ]
let attributedText = NSAttributedString(string: "Press me", attributes: attributes)
button.setAttributedTitle(attributedText, for: .normal)

If I scale the font size up and down using Accessibility Inspector, the button's size and label text doesn’t scale properly.

If I just call button.setTitle() passing an ordinary string, though, dynamic type scaling works fine.

Using the same pattern for attributed text directly on a UILabel works fine… it just seems to be when I use attributed text for a UIButton’s title.

Any thoughts or suggestions would be awesome. Thanks

Edit: After a bit more poking, it looks like what is happening is that the text is trying to scale, but the button's width/height aren't growing with it. If I dial up dynamic type to the largest text size and then create the screen and proceed to shrink the font size, it works OK because the buttons width/height constraints are set to an initially large value. But if I start with a small dynamic type setting and grow larger, the button doesn't accommodate the text size change


Solution

  • If I scale the font size up and down using Accessibility Inspector, the button's size and label text doesn’t scale properly.

    To scale the attributed string label of a UIButton with the Dynamic Type, set the title label as an attributed string first and then put this element in the setAttributedTitle button method.

    About the button size, specify the sizeToFit method of the button in the traitCollectionDidChange instance method of the UITraitEnvironment protocol (using constraints can be another solution as well).

    I created a blank project in Xcode as follows: enter image description here

    Copy-paste the code snippet hereunder (Swift 5.0, iOS 12):

    class ViewController: UIViewController {
    
        @IBOutlet weak var myButton: UIButton!
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
        
            myButton.layer.borderColor = UIColor.black.cgColor
            myButton.layer.borderWidth = 4.0
        
            myButton.contentEdgeInsets = UIEdgeInsets(top: 10,
                                                      left: 20,
                                                      bottom: 10,
                                                      right: 20)
            
            let font = UIFont(name: "Helvetica", size: 19)!
            let scaledFont = UIFontMetrics.default.scaledFont(for: font)
    
            let attributes = [NSAttributedString.Key.font: scaledFont]
            let attributedText = NSAttributedString(string: "Press me",
                                                    attributes: attributes)
            
            myButton.titleLabel?.attributedText = attributedText
            myButton.setAttributedTitle(myButton.titleLabel?.attributedText,
                                        for: .normal)
    
            myButton.titleLabel?.adjustsFontForContentSizeCategory = true
        }
    
    
        override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        
            myButton.sizeToFit()
        }
    }
    

    ... and you get the result hereafter: enter image description here If you need further explanation, I suggest to take a look at this Dynamic Type kind of tutorial that contains {code snippets + illustrations} and at this WWDC detailed summary that deals with building apps with Dynamic Type.

    ⚠️ EDIT 2021/02/15 ⚠️

    Thanks to @Anthony's comment, I have improved this old solution to the new context as follows:

    override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
    
            myButton.layer.borderColor = UIColor.black.cgColor
            myButton.layer.borderWidth = 4.0
    
            let font = UIFont(name: "Helvetica", size: 20)!
            let scaledFont = UIFontMetrics.default.scaledFont(for: font)
    
            let attributes = [NSAttributedString.Key.font: scaledFont]
            let attributedText = NSAttributedString(string: "Press Me Huge Button",
                                                    attributes: attributes)
    
            myButton.titleLabel?.attributedText = attributedText
            myButton.titleLabel?.numberOfLines = 0
            myButton.titleLabel?.textAlignment = .center
            
            myButton.setAttributedTitle(attributedText,
                                        for: .normal)
    
            myButton.titleLabel?.adjustsFontForContentSizeCategory = true
            
            createConstraints()
        }
    

    ... and added constraints between the button and its content:

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        
        DispatchQueue.main.async() {
            self.myButton.setNeedsUpdateConstraints() // For updating constraints.
        }
    }
    
    private func createConstraints() {
        
        myButton.translatesAutoresizingMaskIntoConstraints = false
        myButton.titleLabel?.translatesAutoresizingMaskIntoConstraints = false
        
        let spacing = 10.0
        myButton.titleLabel?.trailingAnchor.constraint(equalTo: myButton.trailingAnchor,
                                                       constant: -CGFloat(spacing)).isActive = true
        myButton.titleLabel?.leadingAnchor.constraint(equalTo: myButton.leadingAnchor,
                                                      constant: CGFloat(spacing)).isActive = true
        myButton.titleLabel?.topAnchor.constraint(equalTo: myButton.topAnchor,
                                                  constant: CGFloat(spacing)).isActive = true
        myButton.titleLabel?.bottomAnchor.constraint(equalTo: myButton.bottomAnchor,
                                                     constant: -CGFloat(spacing)).isActive = true
    }
    

    With some new constraints for the button inside Interface Builder, I finally get those screenshots using the Xcode Environment Overrides pane: enter image description here enter image description here

    ⚠️ EDIT 2022/01/28 ⚠️

    iOS 15 introduced a new button style that provides a native adaptation to the multiline titles and the Dynamic Type features. 🙏

    No code is necessary anymore to reach the initial goal of Craig. 👍