在Swift 3/iOS10中更好地使用自定义字体的动态类型的方法

12

我尝试了两种方法:

方法1:

label.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
label.adjustsFontForContentSizeCategory = true

即使在设置中更改了首选文本大小,此方法仍可正常工作,文本大小会自动更改,甚至在返回应用之前也是如此。但它只适用于系统字体(旧金山字体)。

方法2:

要使用自定义字体,我将扩展添加到UIFontDescriptor中:

//from this answer https://dev59.com/zmIj5IYBdhLWcg3wNyn1#35467158
extension UIFontDescriptor {

    private struct SubStruct {
        static var preferredFontName: String = "Avenir-medium"
    }

    static let fontSizeTable : NSDictionary = [
        UIFontTextStyle.headline: [
            UIContentSizeCategory.accessibilityExtraExtraExtraLarge: 23,
            UIContentSizeCategory.accessibilityExtraExtraLarge: 23,
            UIContentSizeCategory.accessibilityExtraLarge: 23,
            UIContentSizeCategory.accessibilityLarge: 23,
            UIContentSizeCategory.accessibilityMedium: 23,
            UIContentSizeCategory.extraExtraExtraLarge: 23,
            UIContentSizeCategory.extraExtraLarge: 21,
            UIContentSizeCategory.extraLarge: 19,
            UIContentSizeCategory.large: 17,
            UIContentSizeCategory.medium: 16,
            UIContentSizeCategory.small: 15,
            UIContentSizeCategory.extraSmall: 14
        ],
        UIFontTextStyle.subheadline: [
            UIContentSizeCategory.accessibilityExtraExtraExtraLarge: 21,
            UIContentSizeCategory.accessibilityExtraExtraLarge: 21,
            UIContentSizeCategory.accessibilityExtraLarge: 21,
            UIContentSizeCategory.accessibilityLarge: 21,
            UIContentSizeCategory.accessibilityMedium: 21,
            UIContentSizeCategory.extraExtraExtraLarge: 21,
            UIContentSizeCategory.extraExtraLarge: 19,
            UIContentSizeCategory.extraLarge: 17,
            UIContentSizeCategory.large: 15,
            UIContentSizeCategory.medium: 14,
            UIContentSizeCategory.small: 13,
            UIContentSizeCategory.extraSmall: 12
        ],
        UIFontTextStyle.body: [
            UIContentSizeCategory.accessibilityExtraExtraExtraLarge: 53,
            UIContentSizeCategory.accessibilityExtraExtraLarge: 47,
            UIContentSizeCategory.accessibilityExtraLarge: 40,
            UIContentSizeCategory.accessibilityLarge: 33,
            UIContentSizeCategory.accessibilityMedium: 28,
            UIContentSizeCategory.extraExtraExtraLarge: 23,
            UIContentSizeCategory.extraExtraLarge: 21,
            UIContentSizeCategory.extraLarge: 19,
            UIContentSizeCategory.large: 17,
            UIContentSizeCategory.medium: 16,
            UIContentSizeCategory.small: 15,
            UIContentSizeCategory.extraSmall: 14
        ],
        UIFontTextStyle.caption1: [
            UIContentSizeCategory.accessibilityExtraExtraExtraLarge: 18,
            UIContentSizeCategory.accessibilityExtraExtraLarge: 18,
            UIContentSizeCategory.accessibilityExtraLarge: 18,
            UIContentSizeCategory.accessibilityLarge: 18,
            UIContentSizeCategory.accessibilityMedium: 18,
            UIContentSizeCategory.extraExtraExtraLarge: 18,
            UIContentSizeCategory.extraExtraLarge: 16,
            UIContentSizeCategory.extraLarge: 14,
            UIContentSizeCategory.large: 12,
            UIContentSizeCategory.medium: 11,
            UIContentSizeCategory.small: 11,
            UIContentSizeCategory.extraSmall: 11
        ],
        UIFontTextStyle.caption2: [
            UIContentSizeCategory.accessibilityExtraExtraExtraLarge: 17,
            UIContentSizeCategory.accessibilityExtraExtraLarge: 17,
            UIContentSizeCategory.accessibilityExtraLarge: 17,
            UIContentSizeCategory.accessibilityLarge: 17,
            UIContentSizeCategory.accessibilityMedium: 17,
            UIContentSizeCategory.extraExtraExtraLarge: 17,
            UIContentSizeCategory.extraExtraLarge: 15,
            UIContentSizeCategory.extraLarge: 13,
            UIContentSizeCategory.large: 11,
            UIContentSizeCategory.medium: 11,
            UIContentSizeCategory.small: 11,
            UIContentSizeCategory.extraSmall: 11
        ],
        UIFontTextStyle.footnote: [
            UIContentSizeCategory.accessibilityExtraExtraExtraLarge: 19,
            UIContentSizeCategory.accessibilityExtraExtraLarge: 19,
            UIContentSizeCategory.accessibilityExtraLarge: 19,
            UIContentSizeCategory.accessibilityLarge: 19,
            UIContentSizeCategory.accessibilityMedium: 19,
            UIContentSizeCategory.extraExtraExtraLarge: 19,
            UIContentSizeCategory.extraExtraLarge: 17,
            UIContentSizeCategory.extraLarge: 15,
            UIContentSizeCategory.large: 13,
            UIContentSizeCategory.medium: 12,
            UIContentSizeCategory.small: 12,
            UIContentSizeCategory.extraSmall: 12
        ],
        ]

    final class func preferredDescriptor(textStyle: String) -> UIFontDescriptor {

        let contentSize = UIApplication.shared.preferredContentSizeCategory
        let style = fontSizeTable[textStyle] as! NSDictionary
        return UIFontDescriptor(name: SubStruct.preferredFontName, size: CGFloat((style[contentSize] as! NSNumber).floatValue))
    }

}

并在 viewDidLoad() 中:

label.font = UIFont(descriptor: UIFontDescriptor.preferredDescriptor(textStyle: UIFontTextStyle.body.rawValue), size: 0)
NotificationCenter.default.addObserver(self, selector:#selector(self.userChangedTextSize(notification:)), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil)

这里是userChangedTextSize函数:

func userChangedTextSize(notification: NSNotification) {
    label.font = UIFont(descriptor: UIFontDescriptor.preferredDescriptor(textStyle: UIFontTextStyle.body.rawValue), size: 0)
}

这种方法的问题是,文本大小直到用户返回应用程序后才会更改,然后用户将看到旧文本大小更改为新大小,这不理想。

我能否拥有两全其美的方案:背景自动更改大小的自定义字体?


1
在iOS 11上,您可以使用UIFontMetrics。 - BangOperator
4个回答

28
这种方法的问题在于,文本大小在用户返回应用程序之前不会改变,然后用户会看到旧文本大小更改为新大小,这并不理想。
我认同你的想法,这可能是更好的用户体验,但我觉得你有点想太多了。
如果你查看系统提供的应用程序(例如联系人),你会清楚地看到刷新也是直到用户返回应用程序后才进行的。
顺便说一句,你可以对你的代码进行一些重构以适应Swift 3:
extension UIFontDescriptor {

    private struct SubStruct {
        static var preferredFontName: String = "Avenir-medium"
    }

    static let fontSizeTable: [UIFontTextStyle: [UIContentSizeCategory: CGFloat]] = [
        .headline: [
            .accessibilityExtraExtraExtraLarge: 23,
            .accessibilityExtraExtraLarge: 23,
            .accessibilityExtraLarge: 23,
            .accessibilityLarge: 23,
            .accessibilityMedium: 23,
            .extraExtraExtraLarge: 23,
            .extraExtraLarge: 21,
            .extraLarge: 19,
            .large: 17,
            .medium: 16,
            .small: 15,
            .extraSmall: 14
        ],
        .subheadline: [
            .accessibilityExtraExtraExtraLarge: 21,
            .accessibilityExtraExtraLarge: 21,
            .accessibilityExtraLarge: 21,
            .accessibilityLarge: 21,
            .accessibilityMedium: 21,
            .extraExtraExtraLarge: 21,
            .extraExtraLarge: 19,
            .extraLarge: 17,
            .large: 15,
            .medium: 14,
            .small: 13,
            .extraSmall: 12
        ],
        .body: [
            .accessibilityExtraExtraExtraLarge: 53,
            .accessibilityExtraExtraLarge: 47,
            .accessibilityExtraLarge: 40,
            .accessibilityLarge: 33,
            .accessibilityMedium: 28,
            .extraExtraExtraLarge: 23,
            .extraExtraLarge: 21,
            .extraLarge: 19,
            .large: 17,
            .medium: 16,
            .small: 15,
            .extraSmall: 14
        ],
        .caption1: [
            .accessibilityExtraExtraExtraLarge: 18,
            .accessibilityExtraExtraLarge: 18,
            .accessibilityExtraLarge: 18,
            .accessibilityLarge: 18,
            .accessibilityMedium: 18,
            .extraExtraExtraLarge: 18,
            .extraExtraLarge: 16,
            .extraLarge: 14,
            .large: 12,
            .medium: 11,
            .small: 11,
            .extraSmall: 11
        ],
        .caption2: [
            .accessibilityExtraExtraExtraLarge: 17,
            .accessibilityExtraExtraLarge: 17,
            .accessibilityExtraLarge: 17,
            .accessibilityLarge: 17,
            .accessibilityMedium: 17,
            .extraExtraExtraLarge: 17,
            .extraExtraLarge: 15,
            .extraLarge: 13,
            .large: 11,
            .medium: 11,
            .small: 11,
            .extraSmall: 11
        ],
        .footnote: [
            .accessibilityExtraExtraExtraLarge: 19,
            .accessibilityExtraExtraLarge: 19,
            .accessibilityExtraLarge: 19,
            .accessibilityLarge: 19,
            .accessibilityMedium: 19,
            .extraExtraExtraLarge: 19,
            .extraExtraLarge: 17,
            .extraLarge: 15,
            .large: 13,
            .medium: 12,
            .small: 12,
            .extraSmall: 12
        ]
    ]

    final class func preferredDescriptor(textStyle: UIFontTextStyle) -> UIFontDescriptor {
        let contentSize = UIApplication.shared.preferredContentSizeCategory
        let style = fontSizeTable[textStyle]!
        return UIFontDescriptor(name: SubStruct.preferredFontName, size: style[contentSize]!)
    }
}
不需要将其转换为NSDictionaryNSNumber并间接获取floatValue。这样,您的调用站点可以使用以下更易读的代码:
func userChangedTextSize(notification: NSNotification) {
    label.font = UIFont(descriptor: .preferredDescriptor(textStyle: .body), size: 0)

}

编辑:由于我正在进行同样的工作,我改进了上述解决方案(SO上常见的解决方案)为更简单的东西。

import UIKIt

extension UIFont {

    private struct CustomFont {
        static var fontFamily = "Avenir"
    }

    /// Returns a bold version of `self`
    public var bolded: UIFont {
        return fontDescriptor.withSymbolicTraits(.traitBold)
            .map { UIFont(descriptor: $0, size: 0) } ?? self
    }

    /// Returns an italic version of `self`
    public var italicized: UIFont {
        return fontDescriptor.withSymbolicTraits(.traitItalic)
            .map { UIFont(descriptor: $0, size: 0) } ?? self
    }

    /// Returns a scaled version of `self`
    func scaled(scaleFactor: CGFloat) -> UIFont {
        let newDescriptor = fontDescriptor.withSize(fontDescriptor.pointSize * scaleFactor)
        return UIFont(descriptor: newDescriptor, size: 0)
    }

    class func preferredCustomFont(forTextStyle textStyle: UIFontTextStyle) -> UIFont {
        // we are using the UIFontDescriptor which is less expensive than creating an intermediate UIFont
        let systemFontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)

        let customFontDescriptor = UIFontDescriptor.init(fontAttributes: [
            UIFontDescriptorFamilyAttribute: CustomFont.fontFamily,
            UIFontDescriptorSizeAttribute: systemFontDescriptor.pointSize // use the font size of the default dynamic font
        ])

        // return font of new family with same size as the preferred system font
        return UIFont(descriptor: customFontDescriptor, size: 0)
    }

}

用法

func userChangedTextSize(notification: NSNotification) {
    label.font = UIFont.preferredCustomFont(forTextStyle: .headline)
    // or in Bold / Italic:
    // label.font = UIFont.preferredCustomFont(forTextStyle: .headline).bolded
    // label.font = UIFont.preferredCustomFont(forTextStyle: .headline).italicized
}

这绝对更浪漫。第一个版本在UIContentSizeCategory更改时提供了更多控制文本大小的方法:您不必使用默认大小(11...53),这可能会破坏整个UI层次结构,并且在iPhone Plus尺寸上什么都无法阅读。Medium应用以微妙的方式响应UIContentSizeCategory的更改:略微更改字体大小并增加行高。 - ielyamani
1
BonMot 可以监控内容大小类别的变化。但是结果仍然不够平滑。即使使用 viewWillAppear 也不够,因为应用切换器会保留使用先前内容大小的应用程序快照。无论如何,稍后我将研究动画内容大小类别的变化:例如从“大”到“accessibilityExtraExtraExtraLarge”需要通过中间所有的 UIContentSizeCategory,灵感来自于这篇文章 - ielyamani
1
我真的很喜欢这种方法-它还使得拥有多个自定义字体变得非常容易。 - koen
2
@FrederikA.Winkelsdorf 我已经弄清楚如何解决了,我必须使用 traitCollectionDidChange。 - Ben Shabat
好的,很高兴听到你解决了问题!自2017年初以来发生了许多变化,我的帖子可能需要进行一些大修。 - Frederik Winkelsdorf
显示剩余6条评论

3

Frederik's Winkelsdorf 的代码版本,但进行了调整以使用两种不同的字体系列(默认字体系列和加粗字体系列),并附带使用示例。

MyFontExtension.swift

import UIKit
extension UIFontDescriptor {
    private struct FontFamily {
        static var preferredFontNameRegular: String = "Montserrat-Regular"
        static var preferredFontNameBold: String = "Montserrat-Bold"
    }
    
    static let fontSizeTable: [UIFontTextStyle: [UIContentSizeCategory: CGFloat]] = [
    .headline: [
        .accessibilityExtraExtraExtraLarge: 23,
        .accessibilityExtraExtraLarge: 23,
        .accessibilityExtraLarge: 23,
        .accessibilityLarge: 23,
        .accessibilityMedium: 23,
        .extraExtraExtraLarge: 23,
        .extraExtraLarge: 21,
        .extraLarge: 19,
        .large: 17,
        .medium: 16,
        .small: 15,
        .extraSmall: 14
    ],
    .subheadline: [
        .accessibilityExtraExtraExtraLarge: 21,
        .accessibilityExtraExtraLarge: 21,
        .accessibilityExtraLarge: 21,
        .accessibilityLarge: 21,
        .accessibilityMedium: 21,
        .extraExtraExtraLarge: 21,
        .extraExtraLarge: 19,
        .extraLarge: 17,
        .large: 15,
        .medium: 14,
        .small: 13,
        .extraSmall: 12
    ],
    .body: [
        .accessibilityExtraExtraExtraLarge: 53,
        .accessibilityExtraExtraLarge: 47,
        .accessibilityExtraLarge: 40,
        .accessibilityLarge: 33,
        .accessibilityMedium: 28,
        .extraExtraExtraLarge: 23,
        .extraExtraLarge: 21,
        .extraLarge: 19,
        .large: 17,
        .medium: 16,
        .small: 15,
        .extraSmall: 14
    ],
    .caption1: [
        .accessibilityExtraExtraExtraLarge: 18,
        .accessibilityExtraExtraLarge: 18,
        .accessibilityExtraLarge: 18,
        .accessibilityLarge: 18,
        .accessibilityMedium: 18,
        .extraExtraExtraLarge: 18,
        .extraExtraLarge: 16,
        .extraLarge: 14,
        .large: 12,
        .medium: 11,
        .small: 11,
        .extraSmall: 11
    ],
    .caption2: [
        .accessibilityExtraExtraExtraLarge: 17,
        .accessibilityExtraExtraLarge: 17,
        .accessibilityExtraLarge: 17,
        .accessibilityLarge: 17,
        .accessibilityMedium: 17,
        .extraExtraExtraLarge: 17,
        .extraExtraLarge: 15,
        .extraLarge: 13,
        .large: 11,
        .medium: 11,
        .small: 11,
        .extraSmall: 11
    ],
    .footnote: [
        .accessibilityExtraExtraExtraLarge: 19,
        .accessibilityExtraExtraLarge: 19,
        .accessibilityExtraLarge: 19,
        .accessibilityLarge: 19,
        .accessibilityMedium: 19,
        .extraExtraExtraLarge: 19,
        .extraExtraLarge: 17,
        .extraLarge: 15,
        .large: 13,
        .medium: 12,
        .small: 12,
        .extraSmall: 12
    ]
]
    
    final class func preferredDescriptor(textStyle: UIFont.TextStyle, styleBold: Bool = false) -> UIFontDescriptor {
        let contentSize = UIApplication.shared.preferredContentSizeCategory
                
        let fontFamily = styleBold ? FontFamily.preferredFontNameBold : FontFamily.preferredFontNameRegular
        
        guard let style = fontSizeTable[textStyle], let size = style[contentSize] else {
            return UIFontDescriptor(name: fontFamily, size: 16)
        }
        return UIFontDescriptor(name: fontFamily, size: size)
    }
}

myViewController.swift

myBoldLabel.font = UIFont(descriptor: .preferredDescriptor(textStyle: .body, styleBold: true), size: 0)

myNormalLabel.font = UIFont(descriptor: .preferredDescriptor(textStyle: .body), size: 0)

1

以下是Fred用Swift 5回答的内容(我尝试编辑原帖,但编辑队列总是满的):

import UIKit

extension UIFont {

    private struct CustomFont {
        static var fontFamily = "Avenir"
    }

    /// Returns a bold version of `self`
    public var bolded: UIFont {
        return fontDescriptor.withSymbolicTraits(.traitBold)
            .map { UIFont(descriptor: $0, size: 0) } ?? self
    }

    /// Returns an italic version of `self`
    public var italicized: UIFont {
        return fontDescriptor.withSymbolicTraits(.traitItalic)
            .map { UIFont(descriptor: $0, size: 0) } ?? self
    }

    /// Returns a scaled version of `self`
    func scaled(scaleFactor: CGFloat) -> UIFont {
        let newDescriptor = fontDescriptor.withSize(fontDescriptor.pointSize * scaleFactor)
        return UIFont(descriptor: newDescriptor, size: 0)
    }

    class func preferredCustomFont(forTextStyle textStyle: UIFont.TextStyle) -> UIFont {
        // we are using the UIFontDescriptor which is less expensive than creating an intermediate UIFont
        let systemFontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)

        let customFontDescriptor = UIFontDescriptor.init(fontAttributes: [
            UIFontDescriptor.AttributeName.family: CustomFont.fontFamily,
            UIFontDescriptor.AttributeName.size: systemFontDescriptor.pointSize // use the font size of the default dynamic font
        ])

        // return font of new family with same size as the preferred system font
        return UIFont(descriptor: customFontDescriptor, size: 0)
    }

}

1

Swift 4:支持无障碍访问(德国BITV)的自定义缩放字体

//UIFont+CustomScaledFont.swift

import UIKit

extension UIFont {

    /// Scaled and styled version of any custom Font
    ///
    /// - Parameters:
    ///   - name: Name of the Font
    ///   - textStyle: The text style i.e Body, Title, ...
    /// - Returns: The scaled custom Font version with the given textStyle 
    static func scaledFont(name:String, textStyle: UIFont.TextStyle) -> UIFont {

        let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle)

        guard let customFont = UIFont(name: name, size: fontDescriptor.pointSize) else {
            fatalError("Failed to load the \(name) font.")
        }

        return UIFontMetrics.default.scaledFont(for: customFont)
    }
}

在成功将自定义字体添加到您的项目后...
打印可用的字体名称:
for family in UIFont.familyNames {
    for name in UIFont.fontNames(forFamilyName: family) {
        print(name)
    }
 }

设置示例:
myLabel.text = "My scaled custom Font"
myLabel.font = UIFont.scaledFont(name: "MyCustomFontName-Bold", textStyle: .title1)
myLabel.adjustsFontForContentSizeCategory = true   

使用无障碍检查器进行测试(MacOS)


如果应用程序以非默认比例启动,则此方法无效。UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle) 将获取缩放后的值,并使用 scaledFont 方法再次进行缩放。 - zgjie
谢谢,这个解决方案对我非常有效。 - Basel
UIFontMetrics适用于iOS11及以上版本,而问题是关于iOS10的。 - blyscuit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接