4、动态类型与字体适配:UIFontMetrics、preferredContentSizeCategory、自定义字体适配
动态类型,说白了就是让用户自己决定字体大小。
你想想看,视力好的年轻人可能觉得默认字体刚好,但上了年纪的用户呢?或者有低视力障碍的朋友?他们可能需要把字体调大两倍甚至三倍。如果我们的 App 不支持动态类型,那这些用户就只能干瞪眼——或者直接卸载你的 App。
我个人习惯,从 iOS 11 开始就把动态类型作为每个项目的标配。嗯,今天我们就来聊聊怎么做好这件事。
4.1 UIFontMetrics:动态字体的核心工具
UIFontMetrics 是 iOS 11 引入的类。它的作用很简单:根据用户设定的字体大小类别,自动缩放你的字体。
我以前在做一个阅读类 App 时,一开始没用 UIFontMetrics,结果用户反馈说「字体调大了但界面还是那么小」。后来我才意识到,光调 UILabel 的字体大小没用,得用系统提供的缩放机制。
核心用法:
// 获取默认的字体度量对象
let metrics = UIFontMetrics.default
// 基于系统字体创建可缩放字体
let bodyFont = UIFont.preferredFont(forTextStyle: .body)
let scaledFont = metrics.scaledFont(for: bodyFont)
// 应用到控件
label.font = scaledFont
label.adjustsFontForContentSizeCategory = true
这里有个关键点:adjustsFontForContentSizeCategory 这个属性必须设为 true。不然你手动缩放了一次,用户再改字体大小,你的控件不会自动更新。
小技巧: 如果你用的是自定义字体,UIFontMetrics 同样支持。只需要传入你的 UIFont 实例即可。
4.2 preferredContentSizeCategory:监听字体变化
有时候我们需要知道用户当前选择了哪个字体大小类别。比如,当字体特别大时,你可能想调整布局——从水平排列改成垂直排列。
preferredContentSizeCategory 就是干这个的。它是一个 UIContentSizeCategory 类型的值,从 iOS 7 就有了。
// 获取当前字体大小类别
let currentCategory = UIApplication.shared.preferredContentSizeCategory
switch currentCategory {
case .extraSmall:
print("极小字体")
case .large:
print("默认大小")
case .extraExtraExtraLarge:
print("极大字体")
// 这里可以调整布局
default:
break
}
我记得有一次,一个用户反馈说在超大字体模式下,我们的按钮文字被截断了。排查后发现,是因为我们没有监听字体变化事件。
正确的做法是注册通知:
NotificationCenter.default.addObserver(
self,
selector: #selector(contentSizeCategoryDidChange),
name: UIContentSizeCategory.didChangeNotification,
object: nil
)
@objc func contentSizeCategoryDidChange() {
// 更新所有依赖字体大小的控件
updateFonts()
// 必要时调整布局
updateLayout()
}
注意: 不要忘记在 deinit 中移除观察者。虽然 iOS 9 以后系统会自动处理,但手动移除是个好习惯。
4.3 自定义字体适配:别让设计稿白费
很多设计师喜欢用自定义字体,觉得系统字体太普通。但自定义字体有个坑:它不会自动支持动态类型。
说白了,你用了自定义字体,就得自己处理缩放逻辑。
我建议的做法是封装一个工具方法:
extension UIFont {
static func customScaledFont(name: String, textStyle: UIFont.TextStyle) -> UIFont {
// 获取系统为该文本样式建议的字体大小
let systemFont = UIFont.preferredFont(forTextStyle: textStyle)
let pointSize = systemFont.pointSize
// 创建自定义字体
guard let customFont = UIFont(name: name, size: pointSize) else {
return systemFont
}
// 使用 UIFontMetrics 进行缩放
let metrics = UIFontMetrics(forTextStyle: textStyle)
return metrics.scaledFont(for: customFont)
}
}
这样,你只需要调用 UIFont.customScaledFont(name: "YourFontName", textStyle: .body) 就能得到一个支持动态类型的自定义字体。
避坑指南: 我曾经在项目中直接用自定义字体的固定字号,结果在 iPad 上被用户骂惨了。因为 iPad 的默认字体大小和 iPhone 不一样,固定字号在 iPad 上显得特别小。从那以后,我再也不敢忽略 UIFontMetrics 了。
4.4 实战:一个完整的适配示例
我们来看一个完整的例子。假设你有一个标题和一段正文,都用了自定义字体:
class DynamicTypeViewController: UIViewController {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var bodyLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setupDynamicType()
// 监听字体变化
NotificationCenter.default.addObserver(
self,
selector: #selector(contentSizeDidChange),
name: UIContentSizeCategory.didChangeNotification,
object: nil
)
}
func setupDynamicType() {
// 标题使用大标题样式
titleLabel.font = UIFont.customScaledFont(
name: "YourTitleFont",
textStyle: .largeTitle
)
titleLabel.adjustsFontForContentSizeCategory = true
// 正文使用正文样式
bodyLabel.font = UIFont.customScaledFont(
name: "YourBodyFont",
textStyle: .body
)
bodyLabel.adjustsFontForContentSizeCategory = true
}
@objc func contentSizeDidChange() {
// 字体变化时,UILabel 会自动更新
// 但如果你有自定义布局,需要在这里调整
adjustLayoutForCurrentCategory()
}
func adjustLayoutForCurrentCategory() {
let category = UIApplication.shared.preferredContentSizeCategory
if category >= .accessibilityMedium {
// 当字体达到无障碍大小时,改为垂直布局
// 比如把图片和文字从水平排列改成垂直排列
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
4.5 常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 字体不随系统变化 | 忘了设置 adjustsFontForContentSizeCategory | 设置为 true |
| 自定义字体缩放后模糊 | 字体文件本身不支持缩放 | 使用可变字体(Variable Font)或换用系统字体 |
| 布局在超大字体下错乱 | 没有根据字体大小调整约束 | 使用 UIStackView 或动态约束 |
| UITextView 不支持动态类型 | 默认不开启 | 设置 adjustsFontForContentSizeCategory = true |
我的经验: 在 Xcode 的 Interface Builder 中,你可以直接预览不同字体大小下的效果。在 Assistant Editor 中,选择 Preview,然后点击左下角的「+」添加不同的字体大小预览。这样不用跑模拟器就能看到效果,省时省力。
动态类型这件事,说白了就是「尊重用户的选择」。用户觉得字体小,你就给他放大;用户觉得字体大,你就给他缩小。别替用户做决定。
嗯,做好动态类型适配,你的 App 在无障碍方面就已经及格了。下一章我们会聊聊 VoiceOver 的支持,那才是真正的进阶内容。