Skip to content

13功能组件:如何设置动态字体,提升视力辅助功能?

如今在 App 开发当中,支持动态字体已成为标配。 2019 年 Airbnb 统计,有 30% 的 iOS 用户没有使用默认的字体大小。这说明什么呢?说明越来越多的用户更喜欢依据自己的习惯来设置字体的大小来符合他们的阅读习惯。

那什么是动态字体(Dynamic Type)呢?动态字体实际上就是允许用户选择屏幕上显示文本内容的大小。它能帮助一些用户把字体变大来提高可读性,也能方便一些用户把字体变小,使得屏幕能显示更多内容。

以上就是动态字体的效果,一般在设置 App ->辅助功能 ->显示与字体大小 ->更大字体里面通过拖动滑动条来改变系统字体的大小。

目前流行的 App 都已经支持动态字体,假如我们的 App 不支持,当用户在不同 App 之间切换的时候就会感觉到很唐突,甚至会因为阅读体验的问题而直接删除。

支持动态字体

那么怎样才能让 iOS App 支持动态字体呢?我们需要为显示文本的组件,例如UILabelUITextViewUIButton指定能自动调整大小的字体。比如下面是为UILabel增加动态字体支持的代码。

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

首先,我们使用了UIFont.UITextStyle.body来创建一个UIFont的实例并赋值给 Label 的font属性。 然后把该 Label 的adjustsFontForContentSizeCategory设置为true来让它响应用户的动态字体设置。这个属性默认值就为true,假如我们不想让文本自动支持动态字体,可以把它设为false

目前,iOS 系统为我们提供了Large TitleTitle 1Body 等 11 种字体风格,你可以在苹果官方的 《Human Interface Guidelines 》文档里查看它们的具体规范。如下图所示,我从中截取当用户选择默认大小的情况下各种字体风格所对应的字体粗细和大小等信息。其中Large Title的字号是 34pt,Title1 是 28pt,它们的字体粗细都是"Regular"。

Dynamic Type Sizes(来源:Human Interface Guidelines)

为第三方字体库加入动态字体支持

绝大多数情况下,我们应该使用 iOS 系统提供的内置字体库。但也有一些例外,例如使用自定义字体库来强调自身品牌,或者使用搞怪字体为游戏提供沉浸式体验。这个时候怎么办呢?我们可以使用第三方字体库,同时为它配置动态字体的支持。代码示例如下:

swift
guard let customFont = UIFont(name: "CustomFont", size: UIFont.labelFontSize) else {
    fatalError("Failed to load the "CustomFont" font. Make sure the font file is included in the project and the font name is spelled correctly."
    )
}
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true

第一步是通过传递字体库的名字,来加载并初始化类型为UIFont的对象customFont

第二步是传入字体风格.headline,来初始化一个UIFontMetrics的对象。

第三步是把customFont传入scaledFont(for font: UIFont) -> UIFont方法,并把返回值赋给 Label 的font。这样label就能即使用第三方的字体库又能支持动态字体。

Moments App 的字体定义

和大部分的 App 一样,我们没有在 Moments App 里使用第三方字体库。而是根据 07 讲的设计规范,在 DesignKit 组件里面实现了自定义的字体集合,具体代码如下:

swift
public extension UIFont {
    static let designKit = DesignKitTypography()
    struct DesignKitTypography {
        public var display1: UIFont {
            scaled(baseFont: .systemFont(ofSize: 42, weight: .semibold), forTextStyle: .largeTitle, maximumFactor: 1.5)
        }
        public var display2: UIFont {
            scaled(baseFont: .systemFont(ofSize: 36, weight: .semibold), forTextStyle: .largeTitle, maximumFactor: 1.5)
        }
        public var title1: UIFont {
            scaled(baseFont: .systemFont(ofSize: 24, weight: .semibold), forTextStyle: .title1)
        }
        public var title2: UIFont {
            scaled(baseFont: .systemFont(ofSize: 20, weight: .semibold), forTextStyle: .title2)
        }
        public var title3: UIFont {
            scaled(baseFont: .systemFont(ofSize: 18, weight: .semibold), forTextStyle: .title3)
        }
        public var title4: UIFont {
            scaled(baseFont: .systemFont(ofSize: 14, weight: .regular), forTextStyle: .headline)
        }
        public var title5: UIFont {
            scaled(baseFont: .systemFont(ofSize: 12, weight: .regular), forTextStyle: .subheadline)
        }
        public var bodyBold: UIFont {
            scaled(baseFont: .systemFont(ofSize: 16, weight: .semibold), forTextStyle: .body)
        }
        public var body: UIFont {
            scaled(baseFont: .systemFont(ofSize: 16, weight: .light), forTextStyle: .body)
        }
        public var captionBold: UIFont {
            scaled(baseFont: .systemFont(ofSize: 14, weight: .semibold), forTextStyle: .caption1)
        }
        public var caption: UIFont {
            scaled(baseFont: .systemFont(ofSize: 14, weight: .light), forTextStyle: .caption1)
        }
        public var small: UIFont {
            scaled(baseFont: .systemFont(ofSize: 12, weight: .light), forTextStyle: .footnote)
        }
    }
}
private extension UIFont.DesignKitTypography {
    func scaled(baseFont: UIFont, forTextStyle textStyle: UIFont.TextStyle = .body, maximumFactor: CGFloat? = nil) -> UIFont {
        let fontMetrics = UIFontMetrics(forTextStyle: textStyle)
        if let maximumFactor = maximumFactor {
            let maximumPointSize = baseFont.pointSize * maximumFactor
            return fontMetrics.scaledFont(for: baseFont, maximumPointSize: maximumPointSize)
        }
        return fontMetrics.scaledFont(for: baseFont)
    }
}

我们为UIFont定义了一个类型扩展(Extension)。为了调用的时候具有命名空间,我们在这个扩展里面定义了一个名叫DesignKitTypography的内嵌结构体(Nested struct),然后定义了一个静态属性来引用该结构体。

根据之前设计规范里面的字体定义,我们在DesignKitTypography结构体里面分别定义了display1display2title1等一系列的字体属性。比如display1用于页面唯一的大标题,title1用于第一级段落标题,body用于正文等等,它们都调用了同一个私有方法scaled(baseFont: UIFont, forTextStyle textStyle: UIFont.TextStyle = .body, maximumFactor: CGFloat? = nil)来生成一个支持动态字体的UIFont。这里的scaled方法是怎样实现的呢?

首先,该方法通过传递进来的textStyle参数初始化一个UIFontMetrics对象。这样能保证我们自定义的字体会以 iOS 自带的TextStyle作为基准来进行缩放,然后判断maximumFactor是否为空。

如果不为空就计算出maximumPointSize并调用scaledFont(for font: UIFont, maximumPointSize: CGFloat)方法来返回一个UIFont的实例。例如,为了大号的字体display1display2不会无限放大,我们在生成它们的时候把maximumFactor设置为1.5。如果maximumFactor为空,我们就调用scaledFont(for font: UIFont)方法并直接返回UIFont的实例。

有了DesignKitTypography结构体的定义,以后需要增加新的字体类型也非常简单,只需要定义新字体的名字、字体粗细和大小就可以了。例如在这里我新增caption2的代码,它也使用了系统自带的字体库,并把字体大小设为10pt,字体粗细设为细体,同时使用了.caption2作为基准字体风格。 代码示例如下:

swift
public var caption2: UIFont {
    scaled(baseFont: .systemFont(ofSize: 10, weight: .light), forTextStyle: .caption2)
}

完成了这些字体集合的定义以后,我们可以在代码中很方便地使用它们。代码如下:

swift
label1.font = UIFont.designKit.title1
button.titleLabel?.font = UIFont.designKit.bodyBold

我们可以通过UIFont.designKit取出支持动态字体的UIFont类型并赋值给对应的font属性即可,例如UILabelfont属性以及UIButtontitleLabel

测试动态字体

当我们的 App 支持了动态字体以后,在开发过程中需要及时测试,否则可能会不小心引入 UI 的 Bug。幸运的是 Xcode 为我们带来一个名叫Accessibility Inspector的工具来简化动态字体的测试流程。

怎么使用它呢?请看下面的动图:

它使用方法很简单,我们可以在Accessibility Inspector工具里选择运行 Moments App 的 Simulator,然后点击 Settings 按钮,接着拖动滑动条来改变 Font size 的大小,以此来测试 App 对动态字体的响应情况。

总结

这一讲我主要介绍了如何支持动态字体,同时以 Moments App 为例,介绍了如何实现自定义的字体集合。

最后,结合我经验,在加入了动态字体支持后,建议你需要注意以下几点。

  1. 要经常使用Accessibility Inspector工具来测试带文本内容的 UI,保证所有文本都能正常显示。

  2. 不要硬编码文本组件所在容器的高度和宽度,容器的高度和宽度应该随着文本的大小而伸缩,否则当用户选择大字体的时候,可能导致部分文本被遮挡。

  3. 除了特殊情况,不要硬编码UILable组件文本显示的行数,否则可能导致文本显示不全。

  4. 并不是所有文本都需要支持动态字体,例如 Tabbar 上的标题就需要指定静态的字体大小。

思考题:

请结合前几讲所学的内容,实现下面视频中的功能,该功能会列举 iOS 系统自动的所有TextStyle,并把它们在当前动态字体配置下的字体大小显示出来。

这个练习能帮助你把所学的知识结合起来并灵活运用,你可以把实现的代码通过 PR 的方式来提交,有问题可以写到下面的留言区哦。我们下一讲将介绍如何定义语义色来支持深色模式。

源码地址:

自定义字体集合的文件地址:https://github.com/lagoueduCol/iOS-linyongjian/blob/main/Frameworks/DesignKit/src/Font/UIFontExtensions.swift