UIButton 的 attributeTitle 的动态类型

Dynamic type for UIButton's attributeTitle

有人在 UIButton 上为 attributedTitle 使用过动态类型吗?考虑下面的超级简单代码:

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)

如果我使用辅助功能检查器放大和缩小字体大小,按钮的大小和标签文本无法正确缩放。

不过,如果我只是调用 button.setTitle() 并传递一个普通字符串,动态类型缩放就可以正常工作。

直接在 UILabel 上对属性文本使用相同的模式效果很好……当我对 UIButton 的标题使用属性文本时似乎是这样。

任何想法或建议都会很棒。谢谢

编辑: 经过更多的探索,看起来发生的事情是文本试图缩放,但按钮的 width/height 没有增长用它。如果我将动态类型拨到最大文本大小,然后创建屏幕并继续缩小字体大小,它工作正常,因为按钮 width/height 约束设置为初始较大的值。但是,如果我从一个小的动态类型设置开始,然后变大,按钮将无法适应文本大小的变化

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

要将 UIButton 的属性字符串标签缩放到 Dynamic Type首先将标题标签设置为属性字符串,然后放置此元素在 setAttributedTitle 按钮方法中。

关于按钮大小,在UITraitEnvironment协议的traitCollectionDidChange实例方法中指定按钮的sizeToFit方法(使用约束也可以是另一种解决方案).

我在Xcode中新建了一个空白项目如下:

Copy-paste 下面的代码片段 (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()
    }
}

... 之后你会得到结果: 如果您需要进一步的解释,我建议您看一下这个 Dynamic Type kind of tutorial that contains {code snippets + illustrations} and at this WWDC detailed summary,它涉及使用动态类型构建应用程序。

⚠️ 编辑 2021/02/15 ⚠️

感谢@Anthony 的评论,我已针对新上下文改进了这个旧解决方案,如下所示:

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()
    }

...并在按钮及其内容之间添加约束:

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
}

Interface Builder 中的按钮有了一些新的约束,我终于使用 Xcode Environment Overrides pane 获得了这些屏幕截图:

⚠️ 编辑 2022/01/28 ⚠️

iOS 15 介绍了new button style that provides a native adaptation to the multiline titles and the Dynamic Type的特点。

无需任何代码即可实现 Craig 的初始目标。

以防万一有人跟进,解决这个问题的关键包含在@XLE_22的post的第一段中,但非常微妙。

在按钮上调用 setAttributedTitle() 时,您需要从 标签 传递属性字符串。 IE。 myButton.titleLabel?.attributedText... 传递局部变量 attributedText 不起作用。我无法解释为什么会这样。考虑以下代码:

myButton.titleLabel?.attributedText = attributedText
let labelAttributedText = myButton.titleLabel?.attributedText

myButton.setAttributedTitle(labelAttributedText, for: .normal)  // works
myButton.setAttributedTitle(attributedText, for: .normal)       // doesn't work

如果我比较attributedTextlabelAttributedText,我可以看出前者是NSConcreteAttributedString而后者是NSConcreteMutableAttributedString。问他们是否相等 return true 并且快速比较它们的属性似乎表明它们确实相同,但必须有一些细微的不同。

我最初尝试将 attributedText 创建为可变属性字符串,但这也无济于事。非常令人费解,但非常感谢@XLE_22 的提示......我怀疑我自己是否会尝试过这个特定的技巧。

在 Swift 5.5 & XCode 13

如果您使用的是自定义字体。也许这段代码会对你有所帮助。

class DynamicSizeButton: UIButton {
func customStyle() {
    guard let currentFont = self.titleLabel?.font, let customFont = UIFont(name: currentFont.fontName, size: currentFont.pointSize) else {return}
    self.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont)
    self.titleLabel?.adjustsFontForContentSizeCategory = true
}

override func awakeFromNib() {
    super.awakeFromNib()
    customStyle()
   }
}

使用.......