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
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:
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: 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:
⚠️ 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. 👍
Just in case anyone is following along, the key to resolving this is contained in the first para of @XLE_22's post but is quite subtle.
When calling setAttributedTitle()
on the button, you need to pass the attributed string from the label. ie. myButton.titleLabel?.attributedText
... passing the local variable attributedText
does not work. I can't rationalise why this is the case though. Consider the following code:
myButton.titleLabel?.attributedText = attributedText
let labelAttributedText = myButton.titleLabel?.attributedText
myButton.setAttributedTitle(labelAttributedText, for: .normal) // works
myButton.setAttributedTitle(attributedText, for: .normal) // doesn't work
If I compare attributedText
with labelAttributedText
, I can see that the former is a NSConcreteAttributedString
whereas the latter is a NSConcreteMutableAttributedString
. Asking them if they are equal return true
and a quick comparison of their attributes seems to suggest that they are indeed the same, but there must be something ever so slightly different.
I tried initially creating attributedText
as a mutable attributed string, but that didn't help either. Very puzzling, but a huge thanks to @XLE_22 for the tip... I doubt I would have ever tried that specific trick on my own.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With